HarmonyOS 鸿蒙Next中后台持续定位与长时任务实现
HarmonyOS 鸿蒙Next中后台持续定位与长时任务实现 在开发旅行轨迹记录应用时,需要在应用切到后台后继续记录用户轨迹:
- 应用进入后台后,定位几秒钟就停止了
- 如何让应用在后台持续获取位置信息?
- 后台定位时如何在状态栏显示提示图标?
- 用户点击通知栏如何拉起应用?
原因分析
HarmonyOS 为了保护用户隐私和省电,默认情况下应用进入后台后会暂停定位服务。
要实现后台持续定位,必须满足以下条件:
- 申请后台定位权限:
ohos.permission.LOCATION_IN_BACKGROUND - 申请长时任务权限:
ohos.permission.KEEP_BACKGROUND_RUNNING - 启动长时任务:调用
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: 长时任务申请失败?
原因:
- 未在
module.json5中配置backgroundModes - 未申请
KEEP_BACKGROUND_RUNNING权限
解决: 检查权限配置是否完整
Q4: 后台定位耗电量大?
建议:
- 适当增加定位间隔(如 10-30 秒)
- 非必要时提示用户停止后台定位
- 在 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.json5的abilities中为入口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.LOCATION和ohos.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)}`); // 在此处理位置数据,例如保存到数据库或上传 }); - 注意: 持续的后台定位会显著增加功耗,请务必在
onPageHide或onBackground中合理启动,并在onPageShow或onForeground中考虑暂停或调整定位频率。
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中正确声明所有必需的权限和能力,并遵循生命周期及时启动和停止任务,以优化设备资源使用。

