HarmonyOS技术精讲之Background Tasks Kit(后台任务开发服务)——延迟任务与配额管理进阶

这个 API 经常被用错

HarmonyOS 里 Background Tasks KitWorkScheduler 很多人第一次接触,第一反应就是"这不就是个定时器吗"。官方示例能把任务跑起来,但实际项目里坑不少。真正难的地方是配额管理任务生命周期的控制。

直接用 setTimeoutsetInterval 做后台任务,会发现应用切到后台之后很快就被系统挂起或杀掉。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(),动态决策是否执行。

最佳实践

  1. 任务注册时机放在 UIAbility.onStart() 而不是页面组件里 。页面 aboutToAppear 可能因为页面未完全加载导致 startWork 回调异常。

  2. 每次任务执行完后,主动调用 getAvailableQuota() 检查配额 。如果为 0,正确调用 stopWork,不要无限等待。系统不会自动停止已经注册但配额的。

  3. 合理设置 repeatMinuteintervalMinute 。这两个值不是"在 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:系统会在每天零点重置配额,或者用户重启设备后也会重置。跨天不重置,需要等到下个周期。所以当天配额用完就真的用完了,没有补充机制。