HarmonyOS APP长时任务:让应用在后台持续运行的正确姿势
HarmonyOS APP长时任务:让应用在后台持续运行的正确姿势
📌 核心要点:长时任务通过
backgroundTaskManager.startBackgroundRunning()申请,必须在通知栏显示持续通知,适用于音频播放、持续定位、蓝牙连接等用户明确感知的场景。
一、背景与动机
你有没有这样的经历——用网易云音乐听歌,切到微信聊天,音乐还在播放,通知栏还显示着一个"正在播放"的小通知条?这就是长时任务在发挥作用。
再比如高德地图导航,你切到别的 App 看消息,导航语音还在继续播报,GPS 还在持续追踪你的位置。如果切后台就停止导航,那导航还有什么意义?
这些场景有一个共同特点:用户明确知道应用在后台干活,而且期望它一直干下去。HarmonyOS 把这类需求定义为"长时任务"(Continuous Task),它允许应用在退到后台后继续运行,但有一个硬性要求——必须在通知栏告诉用户"我还在后台运行"。
为什么必须显示通知?因为长时任务会持续占用 CPU、网络、GPS 等系统资源,如果偷偷在后台跑,用户完全不知情,手机发烫耗电却找不到元凶。通知栏显示就是给用户一个"知情权"——你可以随时知道谁在后台跑,不想让它跑就一键关掉。
这就是长时任务的设计哲学:给你后台运行的能力,但必须对用户透明。
二、核心原理
2.1 长时任务生命周期
长时任务从申请到释放,有一个完整的生命周期:
flowchart TD
A([应用前台运行]) --> B{需要后台持续运行?}
B -->|是| C[申请长时任务]
B -->|否| A
C --> D[系统校验权限]
D -->|权限通过| E[显示通知栏]
D -->|权限不足| F[申请失败,抛出异常]
E --> G[应用退到后台]
G --> H[长时任务保活运行]
H --> I{任务是否完成?}
I -->|否| H
I -->|是| J[取消长时任务]
J --> K[通知栏消失]
K --> A
H --> L{用户从通知栏关闭?}
L -->|是| M[系统强制停止任务]
M --> N[应用收到回调]
N --> A
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
class A,B,G,H primary
class C,E,J,K info
class F,M,N error
class D,L warning
class I purple
2.2 长时任务类型
HarmonyOS 定义了多种长时任务类型,每种类型对应不同的后台行为和资源需求:
| 类型 | 枚举值 | 说明 | 典型场景 |
|---|---|---|---|
| 数据传输 | DATA_TRANSFER |
网络数据持续传输 | 文件上传/下载 |
| 音频播放 | AUDIO_PLAYBACK |
后台音频播放 | 音乐播放、有声书 |
| 音频录制 | AUDIO_RECORDING |
后台录音 | 录音笔、语音备忘录 |
| 定位导航 | LOCATION |
持续获取位置信息 | 导航、运动追踪 |
| 蓝牙交互 | BLUETOOTH_INTERACTION |
蓝牙持续连接 | 智能手环、蓝牙耳机 |
| 多设备互联 | MULTI_DEVICE_CONNECTION |
多设备协同 | 投屏、文件互传 |
| 任务切换 | TASK_KEEPING |
计算类任务 | 后台编译、数据处理 |
| 实时通话 | VOIP |
网络语音通话 | 视频会议、语音通话 |
2.3 长时任务与通知栏的绑定机制
长时任务和通知栏是强绑定的关系——没有通知栏,就没有长时任务。
graph TB
subgraph 应用进程
A[startBackgroundRunning] --> B[申请长时任务]
B --> C[绑定 NotificationRequest]
end
subgraph 系统服务
D[后台任务管理服务] --> E[校验权限]
E --> F[注册长时任务]
F --> G[通知管理服务]
G --> H[显示持续通知]
end
subgraph 用户交互
H --> I[通知栏显示运行状态]
I --> J[用户点击通知 → 打开应用]
I --> K[用户划掉通知 → 停止任务]
end
C --> D
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
class A,B,C primary
class D,E,F,G info
class H,I,J,K warning
三、代码实战
3.1 音乐播放器长时任务
这是最经典的长时任务场景——音乐后台播放。我们实现一个完整的音乐播放器后台保活方案:
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
import notificationManager from '@ohos.notificationManager';
import { BusinessError } from '@ohos.base';
import wantAgent from '@ohos.app.ability.wantAgent';
/**
* 音乐播放器后台任务管理器
* 封装长时任务的申请、通知栏显示、取消等完整流程
*/
export class MusicBackgroundTaskManager {
/** 长时任务是否正在运行 */
private isRunning: boolean = false;
/** 通知ID,用于更新和取消通知 */
private readonly NOTIFICATION_ID: number = 1001;
/** 长时任务类型:音频播放 */
private readonly BG_MODE = backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK;
/**
* 启动音乐播放长时任务
* @param context UIAbilityContext
* @param songName 当前播放歌曲名
*/
async startMusicTask(context: Context, songName: string): Promise<void> {
if (this.isRunning) {
console.info('[音乐后台] 长时任务已在运行,无需重复申请');
return;
}
try {
// 第一步:创建 WantAgent,点击通知时打开应用
const wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [
{
bundleName: context.abilityInfo.bundleName,
abilityName: context.abilityInfo.name,
}
],
requestCode: 0,
operationType: wantAgent.OperationType.START_ABILITY,
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG],
};
const agent = await wantAgent.getWantAgent(wantAgentInfo);
// 第二步:构建通知请求
const notificationRequest: notificationManager.NotificationRequest = {
id: this.NOTIFICATION_ID,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '正在播放',
text: songName,
}
},
wantAgent: agent,
// 设置为持续通知,用户无法划掉
isOngoing: true,
// 设置通知不可清除
isUnremovable: false,
};
// 第三步:发布通知
notificationManager.publish(notificationRequest);
console.info('[音乐后台] 通知发布成功');
// 第四步:申请长时任务
backgroundTaskManager.startBackgroundRunning(
context,
this.BG_MODE,
notificationRequest
);
this.isRunning = true;
console.info('[音乐后台] 长时任务申请成功,音乐可在后台继续播放');
} catch (error) {
const err = error as BusinessError;
console.error(`[音乐后台] 启动失败,错误码: ${err.code},错误信息: ${err.message}`);
}
}
/**
* 停止音乐播放长时任务
* @param context UIAbilityContext
*/
async stopMusicTask(context: Context): Promise<void> {
if (!this.isRunning) {
console.info('[音乐后台] 长时任务未在运行');
return;
}
try {
// 第一步:取消长时任务
backgroundTaskManager.stopBackgroundRunning(context);
console.info('[音乐后台] 长时任务已取消');
// 第二步:移除通知
notificationManager.cancel(this.NOTIFICATION_ID);
console.info('[音乐后台] 通知已移除');
this.isRunning = false;
} catch (error) {
const err = error as BusinessError;
console.error(`[音乐后台] 停止失败,错误码: ${err.code},错误信息: ${err.message}`);
}
}
/**
* 更新通知栏内容(切歌时使用)
* @param songName 新歌曲名
*/
async updateNotification(songName: string): Promise<void> {
if (!this.isRunning) {
console.warn('[音乐后台] 长时任务未运行,无法更新通知');
return;
}
try {
const notificationRequest: notificationManager.NotificationRequest = {
id: this.NOTIFICATION_ID,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '正在播放',
text: songName,
}
},
isOngoing: true,
};
notificationManager.publish(notificationRequest);
console.info(`[音乐后台] 通知已更新: ${songName}`);
} catch (error) {
const err = error as BusinessError;
console.error(`[音乐后台] 更新通知失败,错误码: ${err.code}`);
}
}
/**
* 获取长时任务运行状态
*/
getTaskStatus(): boolean {
return this.isRunning;
}
}
3.2 运动定位长时任务
运动类 App 需要持续追踪 GPS 位置,即使用户锁屏或切到其他应用也不能中断:
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
import notificationManager from '@ohos.notificationManager';
import geoLocationManager from '@ohos.geoLocationManager';
import { BusinessError } from '@ohos.base';
/**
* 运动定位后台任务管理器
* 支持跑步、骑行等运动场景的持续定位追踪
*/
export class SportLocationTaskManager {
private isTracking: boolean = false;
private locationChangeCallback: ((location: geoLocationManager.Location) => void) | null = null;
private readonly NOTIFICATION_ID: number = 2001;
private totalDistance: number = 0; // 总距离(米)
private lastLocation: geoLocationManager.Location | null = null;
/**
* 启动运动定位长时任务
* @param context UIAbilityContext
* @param sportType 运动类型名称
*/
async startSportTracking(context: Context, sportType: string): Promise<void> {
if (this.isTracking) {
console.info('[运动定位] 已在追踪中');
return;
}
try {
// 1. 构建通知
const notificationRequest: notificationManager.NotificationRequest = {
id: this.NOTIFICATION_ID,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: `${sportType}进行中`,
text: '正在记录您的运动轨迹...',
}
},
isOngoing: true,
};
// 2. 发布通知
notificationManager.publish(notificationRequest);
// 3. 申请长时任务(定位类型)
backgroundTaskManager.startBackgroundRunning(
context,
backgroundTaskManager.BackgroundMode.LOCATION,
notificationRequest
);
// 4. 开启持续定位
const locationRequest: geoLocationManager.ContinuousLocationRequest = {
interval: 1, // 每秒更新一次位置
locationScenario: geoLocationManager.UserActivityScenario.NAVIGATION,
};
this.locationChangeCallback = (location: geoLocationManager.Location): void => {
this.onLocationReceived(location);
};
geoLocationManager.on('locationChange', locationRequest, this.locationChangeCallback);
this.isTracking = true;
this.totalDistance = 0;
this.lastLocation = null;
console.info('[运动定位] 运动追踪已启动');
} catch (error) {
const err = error as BusinessError;
console.error(`[运动定位] 启动失败,错误码: ${err.code},错误信息: ${err.message}`);
}
}
/**
* 位置变化回调
* 计算累计距离并更新通知
*/
private onLocationReceived(location: geoLocationManager.Location): void {
if (this.lastLocation) {
// 计算两点间距离(简化版,使用直线距离)
const distance = this.calculateDistance(
this.lastLocation.latitude, this.lastLocation.longitude,
location.latitude, location.longitude
);
this.totalDistance += distance;
}
this.lastLocation = location;
// 每累计100米更新一次通知
if (Math.floor(this.totalDistance / 100) !== Math.floor((this.totalDistance - 1) / 100)) {
this.updateDistanceNotification();
}
}
/**
* 计算两点间距离(Haversine公式简化版)
*/
private calculateDistance(
lat1: number, lon1: number,
lat2: number, lon2: number
): number {
const R = 6371000; // 地球半径(米)
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* 更新距离通知
*/
private updateDistanceNotification(): void {
try {
const km = (this.totalDistance / 1000).toFixed(2);
const notificationRequest: notificationManager.NotificationRequest = {
id: this.NOTIFICATION_ID,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '运动进行中',
text: `已运动 ${km} 公里`,
}
},
isOngoing: true,
};
notificationManager.publish(notificationRequest);
} catch (error) {
console.error('[运动定位] 更新通知失败');
}
}
/**
* 停止运动追踪
*/
async stopSportTracking(context: Context): Promise<number> {
if (!this.isTracking) {
return 0;
}
try {
// 1. 停止定位
if (this.locationChangeCallback) {
geoLocationManager.off('locationChange', this.locationChangeCallback);
}
// 2. 取消长时任务
backgroundTaskManager.stopBackgroundRunning(context);
// 3. 移除通知
notificationManager.cancel(this.NOTIFICATION_ID);
this.isTracking = false;
console.info(`[运动定位] 运动追踪已停止,总距离: ${(this.totalDistance / 1000).toFixed(2)} 公里`);
return this.totalDistance;
} catch (error) {
const err = error as BusinessError;
console.error(`[运动定位] 停止失败,错误码: ${err.code}`);
return this.totalDistance;
}
}
}
3.3 长时任务保活与异常恢复
在实际生产环境中,长时任务可能因为系统资源回收、用户手动关闭等原因被中断。我们需要一套保活和恢复机制:
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
import notificationManager from '@ohos.notificationManager';
import { BusinessError } from '@ohos.base';
import { AbilityConstant } from '@ohos.app.ability.AbilityConstant';
/**
* 长时任务保活管理器
* 处理长时任务被系统回收后的自动恢复
*/
export class ContinuousTaskKeepAliveManager {
private static instance: ContinuousTaskKeepAliveManager;
private currentTaskType: backgroundTaskManager.BackgroundMode | null = null;
private currentNotificationId: number = 3001;
private taskContext: Context | null = null;
private taskDescription: string = '';
private retryCount: number = 0;
private readonly MAX_RETRY: number = 3;
static getInstance(): ContinuousTaskKeepAliveManager {
if (!ContinuousTaskKeepAliveManager.instance) {
ContinuousTaskKeepAliveManager.instance = new ContinuousTaskKeepAliveManager();
}
return ContinuousTaskKeepAliveManager.instance;
}
/**
* 注册长时任务保活
* 在 Ability 的 onCreate 中调用
*/
registerKeepAlive(
context: Context,
taskType: backgroundTaskManager.BackgroundMode,
description: string
): void {
this.taskContext = context;
this.currentTaskType = taskType;
this.taskDescription = description;
this.retryCount = 0;
console.info('[保活管理] 已注册长时任务保活');
}
/**
* 处理 Ability 生命周期回调
* 在 onForeground 中尝试恢复长时任务
*/
handleAbilityForeground(): void {
if (!this.taskContext || !this.currentTaskType) {
return;
}
// 检查长时任务是否还在运行
try {
const status = backgroundTaskManager.getBackgroundRunningStatusSync();
if (status > 0) {
console.info('[保活管理] 长时任务仍在运行,无需恢复');
return;
}
} catch (error) {
// 查询失败,尝试恢复
}
// 长时任务已丢失,尝试恢复
console.info('[保活管理] 检测到长时任务丢失,尝试恢复...');
this.tryRestoreTask();
}
/**
* 尝试恢复长时任务
*/
private async tryRestoreTask(): Promise<void> {
if (this.retryCount >= this.MAX_RETRY) {
console.error('[保活管理] 已达到最大重试次数,放弃恢复');
return;
}
this.retryCount++;
try {
// 重新构建通知
const notificationRequest: notificationManager.NotificationRequest = {
id: this.currentNotificationId,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '后台任务恢复中',
text: this.taskDescription,
}
},
isOngoing: true,
};
notificationManager.publish(notificationRequest);
// 重新申请长时任务
backgroundTaskManager.startBackgroundRunning(
this.taskContext!,
this.currentTaskType!,
notificationRequest
);
this.retryCount = 0; // 恢复成功,重置重试计数
console.info('[保活管理] 长时任务恢复成功');
} catch (error) {
const err = error as BusinessError;
console.error(`[保活管理] 第${this.retryCount}次恢复失败,错误码: ${err.code}`);
// 延迟后重试
setTimeout(() => {
this.tryRestoreTask();
}, 2000 * this.retryCount); // 指数退避
}
}
/**
* 处理 Ability 回调状态变化
* 当系统因资源回收回调应用时,尝试保活
*/
handleOnCallback(state: AbilityConstant.OnCallbackState): void {
switch (state) {
case AbilityConstant.OnCallbackState.CONTINUOUS_TASK_BEGIN:
console.info('[保活管理] 系统通知:长时任务已开始');
break;
case AbilityConstant.OnCallbackState.CONTINUOUS_TASK_END:
console.warn('[保活管理] 系统通知:长时任务已结束,尝试恢复');
this.tryRestoreTask();
break;
default:
break;
}
}
/**
* 注销保活
* 在 Ability 的 onDestroy 中调用
*/
unregisterKeepAlive(): void {
this.taskContext = null;
this.currentTaskType = null;
this.taskDescription = '';
this.retryCount = 0;
console.info('[保活管理] 已注销长时任务保活');
}
}
/**
* 在 UIAbility 中集成保活机制的示例
*/
export class MyAbility {
private keepAliveManager: ContinuousTaskKeepAliveManager =
ContinuousTaskKeepAliveManager.getInstance();
onCreate(): void {
// 注册保活
this.keepAliveManager.registerKeepAlive(
this as unknown as Context,
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
'音乐播放中'
);
}
onForeground(): void {
// 前台时检查并恢复长时任务
this.keepAliveManager.handleAbilityForeground();
}
onDestroy(): void {
// 注销保活
this.keepAliveManager.unregisterKeepAlive();
}
}
四、踩坑与注意事项
4.1 通知ID冲突
踩坑:多个长时任务使用同一个通知ID,导致通知互相覆盖,系统无法正确关联长时任务和通知。
解决方案:每个长时任务使用不同的通知ID,且保证全局唯一。
// ❌ 错误:多个任务共用通知ID
const NOTIFICATION_ID = 1000; // 所有任务都用1000
// ✅ 正确:不同任务使用不同ID
const MUSIC_NOTIFICATION_ID = 1001;
const LOCATION_NOTIFICATION_ID = 2001;
const BLUETOOTH_NOTIFICATION_ID = 3001;
4.2 忘记取消长时任务
踩坑:应用退出时忘记调用 stopBackgroundRunning(),导致通知栏一直显示,长时任务一直占用资源。
解决方案:在 Ability 的 onDestroy 中确保取消长时任务。
// 在 UIAbility 的生命周期中管理长时任务
onDestroy(): void {
// 确保取消长时任务
try {
backgroundTaskManager.stopBackgroundRunning(this.context);
notificationManager.cancel(NOTIFICATION_ID);
} catch (error) {
console.error('取消长时任务失败');
}
}
4.3 长时任务类型不匹配
踩坑:申请了 AUDIO_PLAYBACK 类型的长时任务,但实际在后台做的是数据计算。系统在审核时可能会判定为"类型不匹配",拒绝上架。
解决方案:长时任务类型必须与实际后台行为一致。音频播放就用 AUDIO_PLAYBACK,定位就用 LOCATION,不要张冠李戴。
4.4 通知内容不规范
踩坑:通知内容写得太模糊,比如只写"正在运行",用户不知道应用在后台干什么。
解决方案:通知内容应该清晰描述后台任务的具体行为。
// ❌ 通知内容模糊
normal: { title: '正在运行', text: '请勿关闭' }
// ✅ 通知内容清晰
normal: { title: '正在播放音乐', text: '周杰伦 - 晴天' }
4.5 在后台申请长时任务
踩坑:应用已经在后台了,才调用 startBackgroundRunning(),系统会拒绝。
解决方案:长时任务必须在前台申请,然后再退到后台。如果应用已经在后台,需要先通过通知或桌面图标把应用拉到前台,再申请。
五、HarmonyOS 6 适配
5.1 API 变更
| 变更项 | HarmonyOS 5.0 | HarmonyOS 6 |
|---|---|---|
| 长时任务申请 | startBackgroundRunning() |
新增重载方法,支持更多配置参数 |
| 通知绑定 | 必须手动构建 NotificationRequest | 新增简化接口,自动生成默认通知 |
| 任务回调 | 无系统级回调 | 新增on('continuousTaskStateChange') 回调 |
| 多任务支持 | 同一应用只能有一个长时任务 | 支持同一应用同时运行多个不同类型长时任务 |
5.2 迁移指南
- 回调监听:6.0 新增了长时任务状态变化回调,可以替代手动查询:
// HarmonyOS 6 新增的回调监听
backgroundTaskManager.on('continuousTaskStateChange', (info) => {
console.info(`[长时任务] 状态变化: ${JSON.stringify(info)}`);
if (info.state === 'end') {
// 长时任务被系统终止,执行恢复逻辑
}
});
- 多任务支持:6.0 允许同一应用同时运行多个长时任务,但每个任务类型只能有一个实例。
- 通知简化:6.0 提供了默认通知模板,可以不手动构建 NotificationRequest:
// HarmonyOS 6 简化接口(示意)
backgroundTaskManager.startBackgroundRunning(context, bgMode, {
title: '正在播放音乐',
text: '周杰伦 - 晴天',
});
六、总结
核心知识点回顾
长时任务(Continuous Task)
├── 核心API
│ ├── startBackgroundRunning() → 申请长时任务
│ ├── stopBackgroundRunning() → 取消长时任务
│ └── getBackgroundRunningStatusSync() → 查询运行状态
│
├── 任务类型(8种)
│ ├── AUDIO_PLAYBACK → 音频播放
│ ├── AUDIO_RECORDING → 音频录制
│ ├── LOCATION → 定位导航
│ ├── BLUETOOTH_INTERACTION → 蓝牙交互
│ ├── MULTI_DEVICE_CONNECTION → 多设备互联
│ ├── TASK_KEEPING → 计算任务
│ ├── DATA_TRANSFER → 数据传输
│ └── VOIP → 实时通话
│
├── 通知栏绑定
│ ├── 必须显示持续通知(isOngoing: true)
│ ├── 通知内容需清晰描述后台行为
│ └── 用户可通过通知栏终止任务
│
├── 保活机制
│ ├── onForeground 时检查任务状态
│ ├── 任务丢失时自动恢复
│ └── 指数退避重试策略
│
└── 开发准则
├── 前台申请,后台运行
├── 任务类型与行为一致
├── 用完即释放
└── 通知内容清晰规范
一句话总结:长时任务是把双刃剑——用好了让应用在后台如鱼得水,用滥了让用户电量如流水。记住"前台申请、通知绑定、类型匹配、用完释放"这十六个字,就能用好长时任务。
- 点赞
- 收藏
- 关注作者
评论(0)