
这个 API 经常被用错
HarmonyOS 里 Background Tasks Kit 的 WorkScheduler 很多人第一次接触,第一反应就是"这不就是个定时器吗"。官方示例能把任务跑起来,但实际项目里坑不少。真正难的地方是配额管理 和任务生命周期的控制。
直接用 setTimeout 或 setInterval 做后台任务,会发现应用切到后台之后很快就被系统挂起或杀掉。WorkScheduler 就是为了解决这个问题,但它的工作方式和你想的可能不太一样。
它解决什么问题
WorkScheduler 不是给你一个无限跑的定时器。系统会根据你设置的条件(网络、电量、时间等),在合适的时机唤醒你的任务。它只保证任务执行,不保证执行时机,而且每个应用都有配额限制。
| 特性 | setTimeout / setInterval |
WorkScheduler |
|---|---|---|
| 后台存活 | 应用切后台后失效 | 系统调度,应用可被唤醒 |
| 执行时机 | 立即执行 | 延迟执行,系统决定时机 |
| 配额限制 | 无 | 有,配额用完不再执行 |
| 条件设置 | 无 | 网络、电量、时间、充电状态 |
适用场景:非紧急的周期任务,比如每日备份、数据同步、清理缓存。
不适合场景:需要准时、高频的后台行为,比如消息推送、实时定位。这些应该用长时任务。
环境说明
text
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机
核心实现:定时备份任务
这个示例实现一个每天凌晨 2 点,在 Wi-Fi 和充电状态下执行的照片备份任务。
1. 创建任务 Worker
WorkScheduler 需要把任务逻辑放在一个独立的文件里,通过 Worker 调用。
typescript
// entry/src/main/ets/workers/BackupWorker.ts
export class BackupWorker {
execute() {
console.info('[BackupWorker] 任务开始执行');
// 模拟备份逻辑
// 这里可以调用 @ohos.file.fs 等 API 进行文件操作
// 注意:Worker 内不能直接使用 UI 相关 API
// 任何修改 UI 的操作都需要通过 postMessage 通知主线程
console.info('[BackupWorker] 备份完成');
}
}
2. 注册延迟任务
typescript
// entry/src/main/ets/entryability/EntryAbility.ts
import { UIAbility, Want } from '@kit.AbilityKit';
import { workScheduler, workInfo } from '@kit.BackgroundTasksKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onCreate(want: Want) {
// 注册任务
this.registerBackupTask();
}
private registerBackupTask() {
// 构造任务信息
const work: workInfo.WorkInfo = {
workId: 1001,
bundleName: 'com.example.myapp', // 替换为自己的包名
abilityName: 'com.example.myapp.BackupWorker',
// 设置触发条件
conditions: {
// 要求 Wi-Fi 连接
networkType: workScheduler.NetworkType.NETWORK_TYPE_WIFI,
// 要求充电状态
chargerType: workScheduler.ChargerType.CHARGING_PLUGGED_AC,
// 每天固定时间触发(24小时制)
repeatMinute: 120, // 凌晨 2 点 = 120 分钟
// 任务执行间隔(分钟),这里设为每天一次
intervalMinute: 1440,
},
};
try {
// 开始调度任务
workScheduler.startWork(work);
console.info('[EntryAbility] 备份任务注册成功');
} catch (error) {
console.error(`[EntryAbility] 注册失败: ${JSON.stringify(error)}`);
}
}
}
注意点:
workId必须唯一。同一个workId重复调用startWork会被忽略。repeatMinute设置的是第一次触发时刻,加上intervalMinute确定后续周期。- 任务注册时机建议放在
onCreate或应用启动后,不要放在页面aboutToAppear里。很多时候页面还没准备好就会出错。
3. 撤销任务
应用或页面销毁时,应该主动清理,避免内存泄漏。
typescript
private cancelBackupTask() {
try {
workScheduler.stopWork(1001, true); // 第二个参数 true 表示立即停止
console.info('[EntryAbility] 任务已撤销');
} catch (error) {
console.error(`[EntryAbility] 撤销失败: ${JSON.stringify(error)}`);
}
}
override onDestroy() {
this.cancelBackupTask();
}
核心实现:配额监听与降级策略
每个应用每天能执行的延迟任务是有限制的。可以在任务执行前检查配额,在配额快用完时调整后台活动频率。
typescript
// entry/src/main/ets/utils/QuotaManager.ts
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
export class QuotaManager {
private static readonly QUOTA_LOW_THRESHOLD = 2; // 配额低于 2 次时降级
static async checkAndAdaptStrategy() {
try {
// 获取当前剩余的配额数量
const quota = await backgroundTaskManager.getAvailableQuota();
console.info(`[QuotaManager] 可用配额: ${quota}`);
if (quota <= 0) {
// 配额已耗尽,暂停所有后台任务
this.disableAllBackgroundTasks();
console.warn('[QuotaManager] 配额耗尽,暂停所有后台任务');
return;
}
if (quota <= this.QUOTA_LOW_THRESHOLD) {
// 配额偏低,降级
this.reduceBackgroundFrequency();
console.info('[QuotaManager] 配额偏低,降低后台活动频率');
return;
}
// 正常触发
this.normalBackgroundTasks();
} catch (error) {
console.error(`[QuotaManager] 检查配额失败: ${JSON.stringify(error)}`);
// 失败时保守策略:降级
this.reduceBackgroundFrequency();
}
}
private static disableAllBackgroundTasks() {
// 把所有未执行的 work 取消
workScheduler.stopWork(1001, false);
workScheduler.stopWork(1002, false);
// 可以清空本地的待执行队列标记
}
private static reduceBackgroundFrequency() {
// 延长任务间隔
// 修改已有任务的 intervalMinute,由原来的 1440 改为 2880
// 或者通过重新注册参数实现
}
private static normalBackgroundTasks() {
// 恢复正常策略
}
}
然后在备份任务执行前调用:
typescript
// 执行前
await QuotaManager.checkAndAdaptStrategy();
// 然后开始备份
常见问题
问题 1:WorkScheduler 在应用被清理后,任务还执行吗?
现象:用户清掉最近任务,发现备份任务不再执行。
原因 :默认情况,WorkScheduler 只在应用进程存活时触发。如果需要进程被清理后仍能执行,需要将任务注册为长时任务,但这又会受更严格的配额和用户授权限制。
解决 :确认需求。普通延迟任务不要依赖后台常驻,真正常驻后台的场景应使用 longTermTasks,但需要向用户明确申请权限。大多数场景,应用只要不主动退出(process.exit),WorkScheduler 能正常工作。被系统清理后,下次用户打开应用时任务会失效,需要重新注册。
问题 2:配额的 getAvailableQuota() 返回的值和实际体验不符
现象 :当天手动测试了几次后,getAvailableQuota() 返回 0,但第二天早上又变成了 2。
原因 :配额是按天计算的。系统每天凌晨会重置配额。这个 API 返回的是当前剩余配额,不是总配额。而且不同设备的配额值不同,测试设备一般配额较低。
解决 :不要在真机频繁调试 WorkScheduler,用模拟器或测试设备。正式业务里,每次执行任务前都调用 getAvailableQuota(),动态决策是否执行。
最佳实践
-
任务注册时机放在
UIAbility.onStart()而不是页面组件里 。页面aboutToAppear可能因为页面未完全加载导致startWork回调异常。 -
每次任务执行完后,主动调用
getAvailableQuota()检查配额 。如果为 0,正确调用stopWork,不要无限等待。系统不会自动停止已经注册但配额的。 -
合理设置
repeatMinute和intervalMinute。这两个值不是"在 02:00 和 每 1440 分钟"的简单组合。repeatMinute是相对于零点 的分钟数。比如设置 120,系统期望在凌晨 2:00 触发一次,然后每intervalMinute分钟一次。如果intervalMinute是 1440,那下次就是明天 2:00。但如果系统在你注册时已经过了 2:00,它会等到明天 2:00 才第一次触发。所以如果要求立即开始第一次,可以不设repeatMinute,只设intervalMinute。
Demo 入口
完整的入口文件 @Entry 示例:
typescript
@Entry
@Component
struct Index {
build() {
Column() {
Button('注册备份任务')
.onClick(() => {
// 触发 EntryAbility 里的注册逻辑
})
Button('检查配额')
.onClick(async () => {
const quota = await backgroundTaskManager.getAvailableQuota();
AlertDialog.show({ message: `剩余配额: ${quota}` })
})
}
}
}
FAQ
Q:为什么模拟器配额定得很低,真机正常?
A:模拟器和开发者设备(Mate 40、P60 等)的配额策略不同。模拟器用于功能验证,配额通常只有个位数。正式发布的应用,真实设备的配额值更高,但具体数值未公开。建议用真机做最终验证。
Q:任务注册成功后,怎么确认它是否生效?
A:可以通过 workScheduler.isWorkRunning(workId) 查询任务是否在执行。注意这个 API 只在任务正在执行时返回 true,空闲状态返回 false。如果想知道任务是否已被调度,目前没有官方 API。建议自己维护一个本地状态标记。
Q:配额被耗尽后,什么时候恢复正常?
A:系统会在每天零点重置配额,或者用户重启设备后也会重置。跨天不重置,需要等到下个周期。所以当天配额用完就真的用完了,没有补充机制。