HarmonyOS WorkScheduler 延迟任务:约束条件、幂等执行与失败续跑
HarmonyOS WorkScheduler 延迟任务:约束条件、幂等执行与失败续跑

“每天凌晨两点上传一次数据”听起来像后台需求,直接交给 WorkScheduler 却很可能得到错误预期:设备可能关机、无网、低电或处于高负载,系统不会承诺在指定秒级时刻唤醒应用。延迟任务解决的是“当网络、电量、充电、存储等条件合适时执行一段非实时工作”,不是替代闹钟,也不是让应用在后台常驻。
本文以“在 Wi-Fi 且充电时分批同步离线笔记”为例,建立 WorkInfo 注册、WorkSchedulerExtensionAbility 回调、检查点幂等、两分钟收口和失败续跑。读完后可以清楚判断什么时候该用延迟任务,什么时候应改用短时任务、长时任务或代理提醒。
1. 先判断业务是否真的适合延迟调度
适合的任务通常具备三个特点:用户不等待即时结果、触发时间允许漂移、工作能被切成小批次。离线索引维护、非紧急同步、缓存整理都属于这一类。
export interface DeferredJobDecision {
requiresExactTime: boolean;
userCanSeeProgress: boolean;
canWaitForConstraints: boolean;
canSplitIntoBatches: boolean;
}
export function shouldUseWorkScheduler(value: DeferredJobDecision): boolean {
return !value.requiresExactTime
&& !value.userCanSeeProgress
&& value.canWaitForConstraints
&& value.canSplitIntoBatches;
}
需要准点通知的业务考虑代理提醒;用户主动开始并持续感知的上传下载考虑长时任务;前台切后台后只需短暂收尾的工作考虑短时任务。类型选择错误,后面再多重试代码也无法保证体验。
2. 接口版本与硬性约束
本文以 Stage 模型、ArkTS、HarmonyOS SDK API 23 为基线,使用 @kit.BackgroundTasksKit。WorkScheduler 首批接口从 API 9 起提供,earliestStartTime 从 API 22 起提供。官方接口约束中有几项必须先写进设计:
| 约束 | 工程含义 |
|---|---|
| 仅 Stage 模型可用 | FA 工程不能照搬示例 |
workId、bundleName、abilityName 必填 |
注册信息必须与应用配置一致 |
| 至少设置一个触发条件 | 不能创建完全无约束的后台工作 |
参数只支持 number/string/boolean |
复杂对象应传业务键,不传整段 JSON |
| 循环间隔至少 2 小时 | 不适合分钟级轮询 |
| 单次后台运行应小于 2 分钟 | 工作必须分批并主动收口 |
“满足约束”也不等于立即执行,系统还会综合性能、功耗和温度等状态安排时机。
3. 在 module.json5 声明专用 ExtensionAbility
调度回调落到 WorkSchedulerExtensionAbility,它需要在模块配置中声明为 workScheduler 类型。名称必须与后续 WorkInfo.abilityName 完全一致。
{
"module": {
"name": "entry",
"type": "entry",
"srcEntry": "./ets/MyAbilityStage.ets",
"extensionAbilities": [
{
"name": "NoteSyncWorkExtension",
"srcEntry": "./ets/workscheduler/NoteSyncWorkExtension.ets",
"type": "workScheduler",
"exported": false
}
]
}
}
该扩展只服务本应用的系统调度,不需要被外部应用直接调用,因此保持 exported: false。实际字段支持范围以当前工程模板和 SDK 配置声明为准。
4. WorkInfo 表达条件,不表达执行时间承诺
示例要求 Wi-Fi、充电,并允许设备重启后恢复注册。earliestStartTime 只表示从申请时刻起最早多久可以触发,不是倒计时结束就必须执行。
import { workScheduler } from '@kit.BackgroundTasksKit';
const NOTE_SYNC_WORK_ID: number = 12001;
export function buildNoteSyncWork(bundleName: string): workScheduler.WorkInfo {
return {
workId: NOTE_SYNC_WORK_ID,
bundleName,
abilityName: 'NoteSyncWorkExtension',
networkType: workScheduler.NetworkType.NETWORK_TYPE_WIFI,
isCharging: true,
chargerType: workScheduler.ChargingType.CHARGING_PLUGGED_ANY,
isPersisted: true,
isRepeat: true,
repeatCycleTime: 2 * 60 * 60 * 1000,
earliestStartTime: 15 * 60 * 1000,
parameters: {
jobType: 'note_delta_sync',
batchSize: 50,
schemaVersion: 1
}
};
}
循环任务间隔取官方允许的下限,生产项目应按真实业务放宽。batchSize 只是提示值,回调还需校验范围,不能直接信任注册参数。
5. 延迟任务闭环从注册到检查点结束

完整闭环不是“注册后等回调”:业务先定义条件和批次;系统把任务放入队列;约束满足时启动 Extension;执行层根据检查点处理未完成部分;成功保存新游标,失败保留旧游标,等待后续调度继续。
import { BusinessError } from '@kit.BasicServicesKit';
import { workScheduler } from '@kit.BackgroundTasksKit';
export function registerNoteSync(bundleName: string): void {
try {
workScheduler.startWork(buildNoteSyncWork(bundleName));
console.info('note sync work registered');
} catch (reason) {
const error = reason as BusinessError;
console.error(`register work failed: code=${error.code}, message=${error.message}`);
}
}
startWork() 返回 void,成功表示任务已加入执行队列,不表示同步已经完成。页面提示应写“已安排后台同步”,而不是“同步成功”。
6. onWorkStart 只负责接收调度并启动受控工作
扩展回调签名返回 void。不要在回调里启动没有边界的常驻循环,也不要依赖 Extension 进程永久存活。下面把一次运行交给独立执行器,并捕获所有异常。
import WorkSchedulerExtensionAbility from '@ohos.WorkSchedulerExtensionAbility';
import { workScheduler } from '@kit.BackgroundTasksKit';
export default class NoteSyncWorkExtension extends WorkSchedulerExtensionAbility {
private readonly runner: NoteBatchRunner = new NoteBatchRunner();
onWorkStart(work: workScheduler.WorkInfo): void {
console.info(`work start: ${work.workId}`);
void this.runner.run(work).catch((reason: Object) => {
console.error(`work execution failed: ${JSON.stringify(reason)}`);
});
}
onWorkStop(work: workScheduler.WorkInfo): void {
console.info(`work stop: ${work.workId}`);
this.runner.cancel(work.workId);
}
}
onWorkStop() 是明确的停止信号,执行器的网络请求和批处理循环都应检查取消状态,而不是只打印一条日志。
7. 参数入口必须再次校验
注册发生在应用侧,回调可能在更晚时间甚至设备重启后发生。执行器不能假设所有参数仍符合当前版本,应核对任务 ID、类型、批量大小和 schema 版本。
interface NoteJobOptions {
batchSize: number;
schemaVersion: number;
}
function parseOptions(work: workScheduler.WorkInfo): NoteJobOptions {
if (work.workId !== NOTE_SYNC_WORK_ID) throw new Error('unexpected work id');
const params = work.parameters ?? {};
if (params['jobType'] !== 'note_delta_sync') throw new Error('unexpected job type');
const rawBatch = params['batchSize'];
const rawSchema = params['schemaVersion'];
const batchSize = typeof rawBatch === 'number' ? Math.round(rawBatch) : 20;
const schemaVersion = typeof rawSchema === 'number' ? Math.round(rawSchema) : 0;
if (batchSize < 1 || batchSize > 100) throw new Error('invalid batch size');
if (schemaVersion !== 1) throw new Error('unsupported schema version');
return { batchSize, schemaVersion };
}
只传基础类型可以降低跨进程序列化风险,但不代表值天然可信。业务版本升级时可通过 schemaVersion 拒绝旧任务,并由前台重新注册。
8. 幂等依赖稳定业务键和提交检查点
系统可能因失败、超时或循环规则再次触发同一工作。服务端请求应携带稳定幂等键,本地只在服务端确认成功后推进游标。
export interface SyncCheckpoint {
cursor: number;
lastCommittedBatch: string;
updatedAt: number;
}
export function buildIdempotencyKey(
deviceScope: string,
fromCursor: number,
toCursor: number
): string {
return `${deviceScope}:${fromCursor}:${toCursor}`;
}
export interface CheckpointStore {
load(): Promise<SyncCheckpoint>;
commit(value: SyncCheckpoint): Promise<void>;
}
不要使用当前时间作为唯一幂等键,否则重试会生成新请求,服务端无法识别重复批次。检查点写入失败时保持旧游标,下一次仍会提交同一幂等键。
9. 单次执行预留收尾时间
官方规范要求延迟任务后台运行小于两分钟。执行器不应跑到最后一毫秒才停止,示例给自己 100 秒工作预算,剩余时间用于保存检查点和释放资源。
class ExecutionWindow {
private readonly deadline: number;
constructor(maxWorkMs: number = 100_000) {
this.deadline = Date.now() + maxWorkMs;
}
canStartNextBatch(reserveMs: number = 10_000): boolean {
return Date.now() + reserveMs < this.deadline;
}
remainingMs(): number {
return Math.max(0, this.deadline - Date.now());
}
}
实际预算还应考虑网络超时。单个请求超时时间不能比整个执行窗口还长,否则无法在停止信号到来时及时收口。
10. 批处理循环每一步都允许取消
执行器按检查点读取一批待同步笔记,成功后推进游标;无数据、预算不足或收到停止信号时结束。失败不在后台无限重试,而是保留检查点等待后续调度。
class NoteBatchRunner {
private cancelled: Set<number> = new Set<number>();
cancel(workId: number): void {
this.cancelled.add(workId);
}
async run(work: workScheduler.WorkInfo): Promise<void> {
const options = parseOptions(work);
const window = new ExecutionWindow();
this.cancelled.delete(work.workId);
while (!this.cancelled.has(work.workId) && window.canStartNextBatch()) {
const count = await this.syncNextBatch(options.batchSize);
if (count === 0) break;
}
}
private async syncNextBatch(batchSize: number): Promise<number> {
// 从检查点读取待同步记录,上传成功后再提交新检查点。
return Math.min(batchSize, 0);
}
}
示例中的数据访问是接口占位,项目应接入自己的 Repository。关键语义是:每批独立提交、循环前后可取消、无数据立即退出。
11. 失败续跑不是在 Extension 内自旋重试
网络波动时立即重试一次可能有价值,但无限指数退避会消耗整个后台窗口。更稳的策略是按错误类别决定:认证错误停止并等待用户处理;网络错误保存游标;服务端重复响应按幂等结果处理;数据格式错误隔离单条记录。
type SyncFailure = 'network' | 'auth' | 'invalid_record' | 'server';
function retryInCurrentWindow(kind: SyncFailure, attempt: number): boolean {
if (kind === 'auth' || kind === 'invalid_record') return false;
return attempt < 2;
}
function retryDelayMs(attempt: number): number {
return Math.min(1000 * 2 ** attempt, 5000);
}
当前窗口放弃后,不修改成功游标,下一次系统调度从检查点继续。这样重启、进程回收和重复触发都不会让已确认批次再次产生副作用。
12. 调度责任边界不让 Extension 承担长期状态

业务计划定义要完成的目标;WorkInfo 只携带基础参数与约束;系统调度器决定何时启动;Extension 在有限窗口内执行批次。长期游标存入持久层,不能只放在 Extension 成员变量里。
export interface DeferredSyncRepository {
nextBatch(cursor: number, limit: number): Promise<Array<PendingNote>>;
upload(notes: Array<PendingNote>, idempotencyKey: string): Promise<void>;
loadCheckpoint(): Promise<SyncCheckpoint>;
saveCheckpoint(value: SyncCheckpoint): Promise<void>;
}
export interface PendingNote {
localId: string;
revision: number;
updatedAt: number;
}
把长期状态放进 Repository 后,Extension 被系统重新创建也能恢复进度。parameters 只携带任务类型和批量策略,不携带数百条待同步数据。
13. 注册、查询与取消要使用同一个任务定义
排查任务是否在队列中可以调用 getWorkStatus() 或 obtainAllWorks()。取消时传入与注册一致的核心字段,并根据是否需要移除任务选择 needCancel。
export async function describeNoteSync(): Promise<string> {
try {
const work = await workScheduler.getWorkStatus(NOTE_SYNC_WORK_ID);
return `work=${work.workId}, repeat=${work.isRepeat}, persisted=${work.isPersisted}`;
} catch (reason) {
return `work not registered: ${JSON.stringify(reason)}`;
}
}
export function removeNoteSync(bundleName: string): void {
workScheduler.stopWork(buildNoteSyncWork(bundleName), true);
}
退出账号、关闭云同步或切换数据域时应取消旧任务,并清理对应检查点。只删除本地游标而保留调度任务,会让 Extension 后续再次启动。
14. 常见误区按调度语义排查
| 现象 | 根因方向 | 修正方式 |
|---|---|---|
| 到点没有立即运行 | 把 earliestStartTime 当闹钟 | 接受系统择机,准点业务用代理提醒 |
| 注册时报参数错误 | 缺少触发条件或名称不匹配 | 核对 WorkInfo 与 module.json5 |
| 每次都重复上传 | 幂等键不稳定、游标提前推进 | 服务端确认后再提交检查点 |
| 后台运行被终止 | 单批过大或循环没有时间边界 | 预留收尾时间并拆小批次 |
| 循环任务无法注册 | 间隔小于 2 小时 | 放宽周期或重新选择任务类型 |
| 设备重启后任务消失 | 未设置 isPersisted |
对允许恢复的任务开启持久注册 |
| 关闭同步后仍被唤起 | 只清数据未取消工作 | stopWork(..., true) 移除任务 |
先确认业务对“时间”的要求,再查系统错误码。延迟任务不会提供精确唤醒承诺,这是能力边界,不是偶发缺陷。
15. 真机验收清单与官方资料
[ ] 业务允许择机执行,并能拆成可重复的小批次
[ ] ExtensionAbility 名称与 WorkInfo.abilityName 完全一致
[ ] WorkInfo 至少设置一个真实触发条件
[ ] parameters 只包含 number、string、boolean
[ ] 循环周期不小于 2 小时
[ ] 每批请求使用稳定幂等键
[ ] 只有服务端确认后才推进本地检查点
[ ] onWorkStop 能阻止新批次并收口在途请求
[ ] 单次运行给保存状态和释放资源预留时间
[ ] 退出账号或关闭功能时移除任务与对应检查点
[ ] 真机覆盖无网、低电、重启、重复触发和执行超时
资料来源:
- 华为开发者参考:workScheduler 延迟任务调度
- 华为应用架构规范:后台任务使用
- 本机
D:/harmonyos/SDK/23/ets/api/@ohos.resourceschedule.workScheduler.d.ts与@ohos.WorkSchedulerExtensionAbility.d.ts,用于核对 API 23 的属性、枚举和回调签名。
WorkScheduler 的稳定用法可以概括为:用约束描述合适时机,用幂等键描述同一批工作,用检查点描述已完成进度,用执行窗口约束后台成本。把它当成系统择机调度器,而不是精确计时器或常驻服务,离线同步才能在重启、断网和重复触发下继续推进而不制造重复副作用。
更多推荐



所有评论(0)