【HarmonyOS】桌面卡片如何持续刷新?CheckMe 的 HarmonyOS WorkScheduler 后台更新方案详解
桌面卡片如何持续刷新?CheckMe 的 HarmonyOS WorkScheduler 后台更新方案详解
摘要
很多 HarmonyOS 服务卡片在前台看起来都能正常刷新,但一旦应用退到后台,数据就容易卡住。CheckMe 在实现桌面监控卡片时,也遇到了同样的问题。本文围绕 WidgetWorkSchedulerAbility、WidgetFormAbility 和 EntryAbility,系统讲清楚为什么后台刷新难、只靠 setInterval 为什么不够,以及如何设计一套更稳定的前后台协同刷新方案。
四个主题适配说明
这篇文章的主方向是 能力增强,核心亮点是通过 BackgroundTasksKit 的 WorkScheduler 解决服务卡片后台刷新问题。它同时符合 创新体验,因为用户可以在不进入 App 的情况下持续看到较新的设备状态;符合 高端精致,因为稳定刷新本身就是高级体验的一部分,避免桌面卡片变成静态摆设;也能关联 安全隐私,因为项目在后台只做本地状态采集和按需推送,并在无卡片时停止刷新任务,减少无意义后台活动。
一、为什么卡片刷新真正的难点在后台
先说一个很现实的问题:服务卡片的难点从来不是“把 UI 显示出来”,而是“让它持续有用”。
以设备监控类卡片为例,如果用户拖了一个 CPU 卡片到桌面,他真正期待的是:
- 数据尽量是新的
- 不需要每次点进 App 才刷新
- 应用在后台时卡片也不要长期卡死
而实际开发里,很多人第一反应是直接在应用里写一个 setInterval,每隔几秒更新一次数据。前台运行时,这样做看起来没什么问题;但一旦应用切到后台,事情就复杂了:
- JS 定时器可能暂停
- 进程可能被冻结
- 系统调度优先级会变化
- 卡片和主应用不一定一直共存
这也是为什么很多卡片 Demo 看起来能跑,但实际体验并不稳定。
二、CheckMe 的刷新目标是什么
在 CheckMe 中,我对卡片刷新的目标比较克制,不追求“绝对实时”,而追求“合理可靠”:
- 应用前台时可以主动高频刷新。
- 卡片显示时可以触发局部刷新。
- 后台时尽量由系统级任务补充刷新。
- 多张卡片统一走一套数据和推送链路。
这四点决定了我不能只依赖某一种刷新策略,而必须做前台和后台协同设计。
三、先看前台刷新:EntryAbility 怎么接管轮询
前台刷新主入口可以放在 EntryAbility 中。这里有一个很关键的方法:
private startWidgetPushTimer(): void {
const appCtx = this.context.getApplicationContext();
WidgetDataService.getInstance().startUpdating(WIDGET_PUSH_INTERVAL_MS, (): Promise<void> => {
return WidgetFormIdRegistry.pushAllFromPreferences(appCtx);
});
}
这个方法的意义是:
- 前台时由主应用统一驱动采集
- 每次采集完成后,再统一推送所有卡片
这里有两个设计点很重要。
1. 先采集,再统一推送
如果每张卡片单独去拉一遍 CPU、内存、网络数据,不仅浪费资源,还会导致各卡片刷新节奏不一致。
2. 推送目标来自注册表
通过 WidgetFormIdRegistry.pushAllFromPreferences() 统一拿到当前所有卡片实例,避免卡片管理分散在多个地方。
四、为什么前台和后台刷新不能混为一谈
在 EntryAbility 中,项目对前后台状态做了明确区分。
前台:
onForeground(): void {
void WidgetFormIdRegistry.getFormCount(this.context.getApplicationContext()).then((n: number): void => {
if (n > 0) {
this.startWidgetPushTimer();
void WidgetDataService.getInstance().updateAllWidgetData().then((): void => {
void WidgetFormIdRegistry.pushAllFromPreferences(this.context.getApplicationContext());
});
} else {
WidgetDataService.getInstance().stopUpdating();
}
});
}
后台:
onBackground(): void {
WidgetDataService.getInstance().stopUpdating();
}
这里的逻辑其实非常清晰:
- 前台时允许应用主动频繁更新
- 后台时不再依赖应用自身定时器
这一步不是“少写代码”,而是主动承认后台刷新和前台刷新是两类问题,应该用不同方案处理。
五、后台刷新为什么要引入 WorkScheduler
项目中真正解决后台刷新问题的核心,是:
它继承自 WorkSchedulerExtensionAbility,也就是说,这不是普通页面逻辑,而是系统调度入口。
核心代码如下:
export default class WidgetWorkSchedulerAbility extends WorkSchedulerExtensionAbility {
onWorkStart(work: workScheduler.WorkInfo): void {
const workId: number = work.workId;
if (workId === WIDGET_WORKER_WORK_ID) {
WidgetDataService.getInstance().updateAllWidgetData()
.then((): void => {
void WidgetFormIdRegistry.pushAllFromPreferences(this.context.getApplicationContext());
})
.catch((): void => {
// ignore
});
}
}
}
这段代码背后的思路非常关键:
不再指望主应用进程始终活着,而是交给系统在合适时机唤醒任务,再执行一次数据采集和卡片更新。
对桌面卡片来说,这一步是从“Demo 刷新”走向“真实可用刷新”的分水岭。
六、WorkScheduler 任务是在哪里注册的
很多人只写了 WorkSchedulerExtensionAbility,但没有注册任务,结果任务根本不跑。示例注册方式如下:
private registerWorkScheduler(): void {
const workInfo: workScheduler.WorkInfo = {
workId: WIDGET_WORKER_WORK_ID,
bundleName: 'com.checkme.app',
abilityName: 'WidgetWorkSchedulerAbility',
isRepeat: true,
repeatCycleTime: WORK_SCHEDULER_INTERVAL_MS
};
workScheduler.startWork(workInfo);
}
这段逻辑通常会在 onAddForm() 中触发。也就是说:
- 当用户第一次添加卡片时
- 系统级后台刷新机制就开始准备了
这点很符合实际业务需求,因为如果一个应用压根没有任何卡片,就没必要持续为后台刷新付出成本。
七、为什么注册和注销都要管
项目里不仅有注册,也有注销逻辑:
onRemoveForm(formId: string): void {
this.formMeta.delete(formId);
void WidgetFormIdRegistry.unregister(this.context.getApplicationContext(), formId);
if (this.formMeta.size === 0) {
this.unregisterWorkScheduler();
WidgetDataService.getInstance().stopUpdating();
}
}
这一点很容易被忽略,但非常重要。因为如果最后一张卡片都被删掉了:
- 就没有继续刷新的必要
- 如果还让后台任务一直跑,会造成无意义资源消耗
所以这套设计并不是“只会启动”,而是完整考虑了资源回收。
八、WidgetDataService 如何保证刷新过程不容易卡死
即便有了 WorkScheduler,如果数据采集层本身设计得不好,后台刷新依然可能不稳定。
项目中这一层由:
负责。
它里边做了两个非常关键的保护:
1. 防重入
if (this.collectBusy) {
return Promise.resolve();
}
如果上一次采集没结束,下一次请求就直接跳过,防止叠加。
2. 超时兜底
return Promise.race([
work,
WidgetDataService.sleepRejectTimeout(WidgetDataService.COLLECT_TIMEOUT_MS)
]).finally((): void => {
this.collectBusy = false;
});
这样即使某个系统调用卡住,也不会把整个刷新链永久拖死。
这一步对后台刷新尤其重要,因为后台任务一旦长时间卡死,用户能感知到的结果就是卡片“再也不更新了”。
九、卡片页为什么还要主动刷新一次
除了前台轮询和后台调度之外,CheckMe 的卡片页本身也做了一个实用设计。
以 CpuWidgetPage.ets 为例,在 aboutToAppear() 时会主动调用:
postCardAction(this, {
action: 'call',
abilityName: 'EntryAbility',
params: {
method: 'refreshWidgets',
formId: this.formId
}
});
这样做的意义是:
- 用户刚把卡片加到桌面
- 或者卡片刚刚被系统重新展示
- 页面能主动请求一次刷新
这种“页面主动拉一次”的机制,能减少冷启动时看到旧数据的概率。
十、CheckMe 这套刷新方案的完整总结
如果把整个刷新方案归纳一下,就是:
前台
EntryAbility启动轮询WidgetDataService统一采集WidgetFormIdRegistry找到所有卡片WidgetFormPushHelper统一推送
后台
WidgetFormAbility注册WorkScheduler- 系统在合适时机拉起
WidgetWorkSchedulerAbility - 再次触发统一采集和统一推送
页面显示时
- 卡片页通过
postCardAction请求局部刷新
这三层叠加后,卡片刷新体验会比单一策略更稳。
十一、这篇文章最值得带走的经验
1. 不要把后台刷新理解成“继续跑一个定时器”
真正可靠的思路是:前台靠应用主动刷新,后台尽量交给系统调度。
2. 刷新体系一定要统一
不要让每张卡片各自刷新、各自采集。统一模型、统一缓存、统一推送,系统才稳。
3. 资源回收也要设计
最后一张卡片删掉后,后台任务也应该停掉。
十二、结语
如果服务卡片只是静态展示,那它的技术价值并不高。但一旦你开始认真处理前台刷新、后台刷新、数据防卡死和多卡片实例管理,它就会变成一个很有工程深度的模块。
CheckMe 在这块的实践也让我更明确了一点:HarmonyOS 的服务卡片真正的挑战不在“做出来”,而在“做稳定”。如果你也在做项目,这绝对是一个很值得展开的专题。

更多推荐



所有评论(0)