HarmonyOS 鸿蒙Next中后台持续定位与长时任务实现

HarmonyOS 鸿蒙Next中后台持续定位与长时任务实现 在开发旅行轨迹记录应用时,需要在应用切到后台后继续记录用户轨迹:

  1. 应用进入后台后,定位几秒钟就停止了
  2. 如何让应用在后台持续获取位置信息?
  3. 后台定位时如何在状态栏显示提示图标?
  4. 用户点击通知栏如何拉起应用?
3 回复

原因分析

HarmonyOS 为了保护用户隐私和省电,默认情况下应用进入后台后会暂停定位服务。

要实现后台持续定位,必须满足以下条件:

  1. 申请后台定位权限ohos.permission.LOCATION_IN_BACKGROUND
  2. 申请长时任务权限ohos.permission.KEEP_BACKGROUND_RUNNING
  3. 启动长时任务:调用 backgroundTaskManager.startBackgroundRunning()

解决方案

1. 配置权限

module.json5 中添加后台定位相关权限:

{
  "requestPermissions": [
    {
      "name": "ohos.permission.LOCATION",
      "reason": "$string:location_reason",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    },
    {
      "name": "ohos.permission.LOCATION_IN_BACKGROUND",
      "reason": "$string:background_location_reason",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "always"
      }
    },
    {
      "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
      "reason": "$string:background_running_reason"
    }
  ]
}

同时在 abilities 中声明后台模式:

{
  "abilities": [
    {
      "name": "EntryAbility",
      "backgroundModes": ["location"]
    }
  ]
}

2. 创建后台定位管理器

// services/BackgroundLocationManager.ets
import { common, abilityAccessCtrl, bundleManager, Permissions, wantAgent, WantAgent } from '@kit.AbilityKit'
import geoLocationManager from '@ohos.geoLocationManager'
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'
import { promptAction } from '@kit.ArkUI'

/**
 * 后台定位管理器
 */
export class BackgroundLocationManager {
  private static instance: BackgroundLocationManager
  private context: common.UIAbilityContext | null = null
  private isRunning: boolean = false
  
  // 配置
  private readonly LOCATION_INTERVAL: number = 5  // 定位间隔(秒)
  
  private constructor() {}
  
  static getInstance(): BackgroundLocationManager {
    if (!BackgroundLocationManager.instance) {
      BackgroundLocationManager.instance = new BackgroundLocationManager()
    }
    return BackgroundLocationManager.instance
  }
  
  initialize(context: common.UIAbilityContext): void {
    this.context = context
  }
}

3. 检查并申请后台定位权限

/**
 * 检查后台定位权限
 */
private async checkBackgroundPermission(): Promise<boolean> {
  if (!this.context) return false
  
  try {
    const atManager = abilityAccessCtrl.createAtManager()
    const bundleInfo = await bundleManager.getBundleInfoForSelf(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
    )
    
    const permission: Permissions = 'ohos.permission.LOCATION_IN_BACKGROUND'
    const grantStatus = await atManager.checkAccessToken(
      bundleInfo.appInfo.accessTokenId,
      permission
    )
    
    return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
  } catch (error) {
    console.error('[BackgroundLocation] 检查权限失败:', error)
    return false
  }
}

/**
 * 申请后台定位权限
 */
private async requestBackgroundPermission(): Promise<boolean> {
  if (!this.context) return false
  
  try {
    const atManager = abilityAccessCtrl.createAtManager()
    const permissions: Array<Permissions> = ['ohos.permission.LOCATION_IN_BACKGROUND']
    
    const result = await atManager.requestPermissionsFromUser(this.context, permissions)
    const granted = result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    
    if (granted) {
      console.info('[BackgroundLocation] ✅ 后台定位权限已授权')
      promptAction.showToast({ message: '后台定位权限已授权', duration: 2000 })
    } else {
      console.warn('[BackgroundLocation] ⚠️ 用户拒绝后台定位权限')
      promptAction.showToast({ message: '需要授权后台定位权限才能在后台记录轨迹', duration: 3000 })
    }
    
    return granted
  } catch (error) {
    console.error('[BackgroundLocation] 申请权限失败:', error)
    return false
  }
}

4. 启动后台长时任务(核心)

这是后台定位的关键步骤! 必须先申请长时任务,否则应用进入后台后定位会停止。

/**
 * 启动后台定位
 */
async startBackgroundLocation(): Promise<void> {
  if (!this.context) {
    console.error('[BackgroundLocation] 未初始化')
    return
  }
  
  if (this.isRunning) {
    console.warn('[BackgroundLocation] 后台定位已在运行')
    return
  }
  
  // 1. 检查并申请后台定位权限
  const hasPermission = await this.checkBackgroundPermission()
  if (!hasPermission) {
    const granted = await this.requestBackgroundPermission()
    if (!granted) {
      console.error('[BackgroundLocation] ❌ 权限申请失败')
      return
    }
  }
  
  try {
    // 2. 创建 WantAgent(点击通知后拉起应用)
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [{
        bundleName: 'com.example.travelmark',  // 替换为你的包名
        abilityName: 'EntryAbility'
      }],
      actionType: wantAgent.OperationType.START_ABILITY,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    }
    const wantAgentObj: WantAgent = await wantAgent.getWantAgent(wantAgentInfo)
    
    // 3. 【关键】申请长时任务
    await backgroundTaskManager.startBackgroundRunning(
      this.context,
      backgroundTaskManager.BackgroundMode.LOCATION,  // 定位类型
      wantAgentObj
    )
    console.info('[BackgroundLocation] ✅ 长时任务已申请')
    
    // 4. 配置后台定位参数
    const requestInfo: geoLocationManager.LocationRequest = {
      // 精度优先
      priority: geoLocationManager.LocationRequestPriority.ACCURACY,
      // 导航场景(持续高精度定位)
      scenario: geoLocationManager.LocationRequestScenario.NAVIGATION,
      // 定位间隔(秒)
      timeInterval: this.LOCATION_INTERVAL,
      distanceInterval: 0,
      maxAccuracy: 0  // 0表示最高精度
    }
    
    // 5. 后台定位回调
    const locationCallback = async (location: geoLocationManager.Location) => {
      console.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
      console.info('[BackgroundLocation] 🌙 后台定位成功')
      console.info(`  坐标: ${location.latitude}, ${location.longitude}`)
      console.info(`  精度: ${location.accuracy}米`)
      console.info(`  时间: ${new Date(location.timeStamp).toLocaleString('zh-CN')}`)
      console.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
      
      // WGS84 转 GCJ-02
      const gcj02 = this.wgs84ToGcj02(location.latitude, location.longitude)
      
      // 上传到后端服务器
      await this.uploadLocationToServer(gcj02, location)
    }
    
    // 6. 启动后台定位监听
    geoLocationManager.on('locationChange', requestInfo, locationCallback)
    
    this.isRunning = true
    console.info('[BackgroundLocation] ✅ 后台定位已启动')
    
    // 通知UI更新状态
    AppStorage.setOrCreate('isBackgroundLocationRunning', true)
    
  } catch (error) {
    const err = error as Record<string, Object>
    console.error(`[BackgroundLocation] ❌ 启动失败: code=${err.code}, message=${err.message}`)
    
    // 启动失败时停止长时任务
    try {
      await backgroundTaskManager.stopBackgroundRunning(this.context)
    } catch (e) {}
  }
}

5. 停止后台定位

/**
 * 停止后台定位
 */
async stopBackgroundLocation(): Promise<void> {
  if (!this.isRunning || !this.context) {
    return
  }
  
  try {
    // 1. 停止定位监听
    geoLocationManager.off('locationChange')
    
    // 2. 停止长时任务(状态栏图标消失)
    await backgroundTaskManager.stopBackgroundRunning(this.context)
    
    this.isRunning = false
    console.info('[BackgroundLocation] ✅ 后台定位已停止')
    
    // 通知UI更新状态
    AppStorage.setOrCreate('isBackgroundLocationRunning', false)
    
  } catch (error) {
    console.error('[BackgroundLocation] 停止失败:', error)
  }
}

/**
 * 获取后台定位状态
 */
isBackgroundLocationRunning(): boolean {
  return this.isRunning
}

6. 上传定位数据到服务器

/**
 * 上传定位数据到后端服务器
 */
private async uploadLocationToServer(
  gcj02: { latitude: number, longitude: number },
  rawLocation: geoLocationManager.Location
): Promise<void> {
  try {
    // 逆地理编码获取地址信息
    let address = ''
    let city = ''
    try {
      const reverseRequest: geoLocationManager.ReverseGeoCodeRequest = {
        latitude: rawLocation.latitude,
        longitude: rawLocation.longitude,
        locale: 'zh'
      }
      const addresses = await geoLocationManager.getAddressesFromLocation(reverseRequest)
      if (addresses && addresses.length > 0) {
        address = addresses[0].placeName || ''
        city = addresses[0].locality || ''
      }
    } catch (e) {
      console.warn('[BackgroundLocation] 逆地理编码失败')
    }
    
    // 构建上传数据
    const locationData = {
      latitude: gcj02.latitude,
      longitude: gcj02.longitude,
      accuracy: rawLocation.accuracy,
      altitude: rawLocation.altitude || 0,
      speed: rawLocation.speed || 0,
      direction: rawLocation.direction || 0,
      address: address,
      city: city,
      location_type: 'background',
      recorded_at: new Date(rawLocation.timeStamp).toISOString()
    }
    
    // 调用API上传
    // await LocationApi.upload(locationData)
    console.info('[BackgroundLocation] 📤 定位数据已上传')
    
  } catch (error) {
    console.error('[BackgroundLocation] 上传失败:', error)
  }
}

7. WGS84 转 GCJ-02

/**
 * WGS84 转 GCJ-02
 */
private wgs84ToGcj02(wgsLat: number, wgsLng: number): { latitude: number, longitude: number } {
  const PI = Math.PI
  const A = 6378245.0
  const EE = 0.00669342162296594323
  
  if (wgsLng < 72.004 || wgsLng > 137.8347 || wgsLat < 0.8293 || wgsLat > 55.8271) {
    return { latitude: wgsLat, longitude: wgsLng }
  }
  
  let dLat = this.transformLat(wgsLng - 105.0, wgsLat - 35.0)
  let dLng = this.transformLng(wgsLng - 105.0, wgsLat - 35.0)
  
  const radLat = wgsLat / 180.0 * PI
  let magic = Math.sin(radLat)
  magic = 1 - EE * magic * magic
  const sqrtMagic = Math.sqrt(magic)
  
  dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI)
  dLng = (dLng * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI)
  
  return { latitude: wgsLat + dLat, longitude: wgsLng + dLng }
}

private transformLat(x: number, y: number): number {
  const PI = Math.PI
  let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x))
  ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0
  ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0
  ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0
  return ret
}

private transformLng(x: number, y: number): number {
  const PI = Math.PI
  let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x))
  ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0
  ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0
  ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0
  return ret
}

8. 在页面中使用

// pages/TrackRecordPage.ets
import { BackgroundLocationManager } from '../services/BackgroundLocationManager'

@Entry
@Component
struct TrackRecordPage {
  @StorageLink('isBackgroundLocationRunning') isRecording: boolean = false
  
  aboutToAppear(): void {
    const context = getContext(this) as common.UIAbilityContext
    BackgroundLocationManager.getInstance().initialize(context)
  }
  
  build() {
    Column({ space: 20 }) {
      Text('轨迹记录')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      
      if (this.isRecording) {
        Row({ space: 8 }) {
          Text('●')
            .fontSize(16)
            .fontColor('#FF0000')
          Text('正在记录轨迹...')
            .fontSize(16)
        }
      }
      
      Button(this.isRecording ? '停止记录' : '开始记录')
        .width('80%')
        .height(50)
        .backgroundColor(this.isRecording ? '#FF4444' : '#4CAF50')
        .onClick(async () => {
          if (this.isRecording) {
            await BackgroundLocationManager.getInstance().stopBackgroundLocation()
          } else {
            await BackgroundLocationManager.getInstance().startBackgroundLocation()
          }
        })
      
      Text('开启后台记录后,可以切换到其他应用\n状态栏会显示定位图标')
        .fontSize(14)
        .fontColor('#666666')
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

效果

控制台日志:

[BackgroundLocation] ✅ 后台定位权限已授权
[BackgroundLocation] ✅ 长时任务已申请
[BackgroundLocation] ✅ 后台定位已启动
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[BackgroundLocation] 🌙 后台定位成功
  坐标: 39.910496, 116.403963
  精度: 18米
  时间: 2024/12/23 14:35:30
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[BackgroundLocation] 📤 定位数据已上传

系统表现:

场景 表现
启动后台定位 状态栏显示定位图标 🔵
切到后台 继续记录轨迹,每5秒一次
点击通知栏 拉起应用回到前台
停止后台定位 状态栏图标消失

常见问题

Q1: 后台定位几秒后就停止了?

原因: 未申请长时任务

解决: 调用 backgroundTaskManager.startBackgroundRunning() 申请长时任务

Q2: 权限申请弹窗不显示?

原因: 用户之前拒绝过权限

解决: 引导用户到设置中手动开启,或使用 openPermissionSettings() 跳转设置页

Q3: 长时任务申请失败?

原因:

  1. 未在 module.json5 中配置 backgroundModes
  2. 未申请 KEEP_BACKGROUND_RUNNING 权限

解决: 检查权限配置是否完整

Q4: 后台定位耗电量大?

建议:

  1. 适当增加定位间隔(如 10-30 秒)
  2. 非必要时提示用户停止后台定位
  3. 在 App 设置页提供定位间隔选项

关键点总结

步骤 说明
1. 权限配置 LOCATION_IN_BACKGROUND + KEEP_BACKGROUND_RUNNING
2. 声明后台模式 backgroundModes: ["location"]
3. 创建 WantAgent 用于点击通知拉起应用
4. 申请长时任务 startBackgroundRunning() 是关键!
5. 启动定位监听 geoLocationManager.on('locationChange')
6. 停止时清理 先停定位,再

更多关于HarmonyOS 鸿蒙Next中后台持续定位与长时任务实现的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


鸿蒙Next中后台持续定位通过LocationKit实现,支持地理围栏与位置订阅。长时任务使用Continuous Task Kit,需在module.json5中声明ohos.permission.KEEP_BACKGROUND_RUNNING权限。系统通过资源调度管理后台任务,应用需遵循生命周期约束,避免过度耗电。

在HarmonyOS Next中实现后台持续定位和长时任务,需要正确使用长时任务管理和位置服务能力。以下是针对您问题的具体实现方案:

1. 解决后台定位停止问题

应用进入后台后默认会被挂起,导致定位停止。您需要申请continuousTask长时任务权限,并在应用切后台时启动它。

步骤:

  • 配置权限:module.json5文件中申请ohos.permission.KEEP_BACKGROUND_RUNNING权限。
  • 声明能力:module.json5abilities中为入口Ability添加backgroundModes配置,声明location后台模式。
    {
      "abilities": [{
        "name": "EntryAbility",
        "backgroundModes": ["location"]
      }]
    }
    
  • 启动长时任务: 在应用生命周期回调(如onBackground)中,调用长时任务管理器。
    import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
    import { BusinessError } from '@ohos.base';
    
    // 启动长时任务
    let id: number = 1; // 任务ID需唯一
    try {
      backgroundTaskManager.startBackgroundRunning(this.context, id).then(() => {
        console.info('Continuous task started successfully.');
      }).catch((err: BusinessError) => {
        console.error(`Failed to start continuous task. Code: ${err.code}, message: ${err.message}`);
      });
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to start continuous task. Code: ${err.code}, message: ${err.message}`);
    }
    
  • 停止长时任务: 应用回到前台时,应及时停止以节省资源。
    backgroundTaskManager.stopBackgroundRunning(this.context).then(() => {
      console.info('Continuous task stopped successfully.');
    }).catch((err: BusinessError) => {
      console.error(`Failed to stop continuous task. Code: ${err.code}, message: ${err.message}`);
    });
    

2. 后台持续获取位置信息

启动长时任务后,您需要配置位置服务以在后台持续获取位置。

步骤:

  • 申请位置权限:module.json5中申请ohos.permission.LOCATIONohos.permission.LOCATION_IN_BACKGROUND权限。
  • 配置位置参数: 创建LocationRequest,设置高优先级和连续定位模式。
    import geoLocationManager from '@ohos.geoLocationManager';
    
    let requestInfo: geoLocationManager.LocationRequest = {
      priority: geoLocationManager.LocationRequestPriority.FIRST_FIX, // 高精度
      scenario: geoLocationManager.LocationRequestScenario.UNSET, // 通用场景
      timeInterval: 5, // 位置上报间隔,单位秒
      distanceInterval: 5, // 位置上报距离间隔,单位米
      maxAccuracy: 10 // 精度要求,单位米
    };
    
  • 注册位置监听: 使用on('locationChange')注册监听,回调函数会在位置更新时触发,即使在后台。
    geoLocationManager.on('locationChange', requestInfo, (location) => {
      console.info(`Background location: ${JSON.stringify(location)}`);
      // 在此处理位置数据,例如保存到数据库或上传
    });
    
  • 注意: 持续的后台定位会显著增加功耗,请务必在onPageHideonBackground中合理启动,并在onPageShowonForeground中考虑暂停或调整定位频率。

3. 在状态栏显示后台运行提示

当长时任务运行时,系统会自动在状态栏显示一个常驻的图标(例如一个圆点或特定应用图标)。这是系统行为,无需开发者额外编码。该图标用于提示用户有应用正在后台执行任务。

4. 点击通知栏拉起应用

您需要创建一个通知,当用户点击该通知时,可以触发一个事件来拉起您的应用。

步骤:

  • 发布通知: 在开始后台定位时,发布一个持续性的通知。
    import notificationManager from '@ohos.notificationManager';
    import { BusinessError } from '@ohos.base';
    
    let notificationRequest: notificationManager.NotificationRequest = {
      content: {
        contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
        normal: {
          title: '轨迹记录中',
          text: '应用正在后台记录您的旅行轨迹',
          additionalText: '点击返回应用'
        }
      },
      id: 1, // 通知ID
      isOngoing: true // 设置为持续通知,用户无法直接滑动清除
    };
    
    notificationManager.publish(notificationRequest).then(() => {
      console.info('Background notification published.');
    }).catch((err: BusinessError) => {
      console.error(`Failed to publish notification. Code: ${err.code}, message: ${err.message}`);
    });
    
  • 配置点击行为: 要使通知可点击并拉起应用,需要在module.json5中配置相应Ability的skills,并确保entities包含"entity.system.home"actions包含"action.system.home"。通常入口Ability已默认配置。
  • 处理点击事件: 当用户点击通知时,系统会默认将应用切换到前台。如果您需要执行特定的页面导航或数据刷新,可以在Ability的onWindowStageCreate或对应Page的aboutToAppear生命周期中,通过want参数来判断是否从通知拉起,并执行相应逻辑。
  • 移除通知: 当应用回到前台或停止后台任务时,应移除对应的通知。
    notificationManager.cancel(1).then(() => {
      console.info('Notification canceled.');
    }).catch((err: BusinessError) => {
      console.error(`Failed to cancel notification. Code: ${err.code}, message: ${err.message}`);
    });
    

总结

实现后台持续定位的关键是结合长时任务管理 (backgroundTaskManager) 和位置服务 (geoLocationManager),并辅以状态栏通知 (notificationManager) 提供用户入口。请务必在module.json5中正确声明所有必需的权限和能力,并遵循生命周期及时启动和停止任务,以优化设备资源使用。

回到顶部