HarmonyOS 7 QuickDock 闪控窗开发实录 04:Background Tasks Kit × Stage:前后台任务切换与窗口异常恢复【鸿蒙心迹】

📅 发布时间:2026/10/5 0:38:24
HarmonyOS 7 QuickDock 闪控窗开发实录 04:Background Tasks Kit × Stage:前后台任务切换与窗口异常恢复【鸿蒙心迹】
03 把 QuickDock 的拖动、侧边暂存和位置持久化做稳定以后窗口这条线终于没有太多悬念。第四篇开始处理真正复杂的业务状态应用进入后台 两个任务并存 不同任务后台策略不同 当前闪控窗切换展示任务 Float Surface 中途丢失 回前台后恢复这一篇我故意没有写成“应用后台后任务继续跑”。因为 HarmonyOS 的后台任务是受系统调度和场景约束的。当前 Background Tasks Kit 提供短时任务、长时任务等机制不同业务要选择匹配的后台模式官方最佳实践里也明确用长时任务保障视频导出、上传、下载等长耗时业务在后台持续执行。所以 QuickDock 04 把两个任务拆开compress_assets_01 本地计算 88% SUSPENDED_POLICY upload_release_02 数据传输 61% RUNNING_BACKGROUND当前闪控窗显示的是upload_release_02本轮统一数据taskId: float_20261002_04 taskCount: 2 job0: compress_assets_01 job0Progress: 88% job0State: SUSPENDED_POLICY job1: upload_release_02 job1Progress: 61% job1State: RUNNING_BACKGROUND activeJob: upload_release_02 mode: FLOAT_VIEW backgroundTaskMode: DATA_TRANSFER backgroundDuration: 107s switchCount: 3 recoveryReason: FLOAT_SURFACE_LOST rebuildCount: 1 rebuildCost: 29ms listenerCount: 1 restoredPosition: x732, y128 status: RECOVERED一、前后台切换以后最不该做的是“所有任务一视同仁”如果 TaskRegistry 里有两个任务本地压缩 数据上传应用进入后台时不能简单全部继续也不能全部暂停应该由任务类型决定策略。QuickDock 增加policy/ └── BackgroundTaskPolicy.ets先把业务类型抽象出来exporttypeQuickTaskKindLOCAL_COMPUTE|DATA_TRANSFERexporttypeBackgroundDecisionKEEP_RUNNING|SUSPEND_POLICYexportclassBackgroundTaskPolicy{resolve(kind:QuickTaskKind):BackgroundDecision{if(kindDATA_TRANSFER){returnKEEP_RUNNING}returnSUSPEND_POLICY}}这是项目自己的决策层。真正申请 HarmonyOS Background Tasks Kit 能力的代码仍然放在 Adapter / Service 里不把系统调用散到页面。二、为什么上传任务可以继续压缩任务这里选择暂停当前 Background Tasks Kit 的长时任务模式包含数据传输等明确场景官方文档也强调后台任务需要按实际业务选择合适机制避免无限制占用资源。所以 QuickDock 本轮对upload_release_02采用DATA_TRANSFER后台策略。而本地压缩compress_assets_01在当前 Phone Demo 里没有强行声明“后台无限执行”而是RUNNING → SUSPENDED_POLICY回前台再恢复。这个设计比文章里一句“后台继续压缩”更符合系统约束。如果未来切到符合其他后台模式的设备与场景再由 Policy 决定是否允许继续。三、TaskRegistry 从单任务升级成两任务但 Float View 仍然只显示一个 activeJob这一篇正式把 Store 从单任务升级成 RegistryexportinterfaceRegistryTask{taskId:stringjobId:stringtitle:stringkind:QuickTaskKind progress:numberstate:RUNNING|RUNNING_BACKGROUND|SUSPENDED_POLICY|COMPLETED}exportclassTaskRegistry{privatetasks:Mapstring,RegistryTasknewMap()privateactiveJobId:stringsetActive(jobId:string):void{if(!this.tasks.has(jobId)){return}this.activeJobIdjobId}}两个任务可以同时存在。但标准闪控窗当前只显示activeJob否则 320×184vp 的小窗很快会变成完整任务管理器。四、进入后台时activeJob 也可能切换进入后台前activeJob: compress_assets_01但压缩任务被策略挂起以后继续显示一个88% SUSPENDED_POLICY意义不大。所以 QuickDock 自动切到upload_release_02 61% RUNNING_BACKGROUND这次switchCount3包含用户手动切一次 后台策略切一次 前台恢复切一次显示任务切换不等于业务任务迁移。只是 Float View 选择不同 Snapshot 显示。五、Stage 生命周期只触发策略不直接改任务内部实现UIAbilityonBackground onForeground不应该直接upload.start() compress.pause()而是通知 CoordinatorexportclassForegroundBackgroundCoordinator{constructor(privateregistry:TaskRegistry,privatepolicy:BackgroundTaskPolicy){}onBackground():void{this.registry.all().forEach((task:RegistryTask){constdecisionthis.policy.resolve(task.kind)if(decisionKEEP_RUNNING){this.registry.markBackgroundRunning(task.jobId)}else{this.registry.suspendByPolicy(task.jobId)}})}}生命周期只是“系统环境发生变化”的信号。真正任务怎么处理由业务 Policy 和对应 Service 决定。六、后台数据传输使用系统能力Window 只是状态出口upload_release_02进入RUNNING_BACKGROUND以后QuickDock 会通过 Background Tasks Kit 的适配层申请匹配的数据传输能力。这里最重要的一条仍然是Background Task ! Float View即使闪控窗短暂不可见上传任务仍然由后台任务机制管理。反过来即使 Float View 还显示系统也不意味着允许任意本地计算无限在后台跑。这两个能力不能互相替代。七、这一篇专门模拟一次 FLOAT_SURFACE_LOST前两篇都是正常显示路径。04 我主动做一个异常任务仍在 Store 正常 Float View Surface 丢失异常标记FLOAT_SURFACE_LOST关键规则重建显示层不重建 TaskRegistry。所以FloatRecoveryCoordinatorexportclassFloatRecoveryCoordinator{privaterebuildCount:number0asyncrebuild(reason:string):Promisevoid{constsnapshotTaskRegistry.shared().activeSnapshot()constpositionWindowSessionStore.shared().lastPosition()awaitFloatViewAdapterFactory.current().rebuildFromSnapshot(snapshot,position)this.rebuildCount}}任务不会重新开始。上传仍然是61%窗口只是重新读取当前 Snapshot。八、异常恢复后 listener 必须仍然只有 1窗口重建最容易带来的副作用是旧 listener 没解绑 新窗口又注册一个下一次进度变化就会收到两份 UI Update。所以恢复以后检查listenerCount1本轮rebuildCount1 listenerCount1如果变成 2恢复不能记成成功。九、位置恢复继续复用第三篇数据窗口 Surface 重建后x732 y128不是重新回默认左上角。第三篇做的WindowSessionStore在这里直接复用。恢复顺序TaskRegistry 当前 activeJob → WindowSessionStore lastPosition → Adapter rebuild → bind single listener → show因此 03 的位置工程并不是独立小功能而是 04 异常恢复真正依赖的基础。十、前后台期间的 107 秒到底发生了什么当前回归数据backgroundDuration107s在这段时间里upload_release_02 36% → 61% RUNNING_BACKGROUND compress_assets_01 88% SUSPENDED_POLICY回前台后compress_assets_01 允许重新 RUNNING如果用户切回压缩任务窗口继续从 88% 显示不会从头开始。这就是 TaskRegistry 独立存在的价值。十一、DevEco 图里要同时看到策略和恢复开发图本轮 HiLogtaskId float_20261002_04 onBackground tasks2 policy upload_release_02 DATA_TRANSFER RUNNING_BACKGROUND policy compress_assets_01 SUSPENDED_POLICY FLOAT_SURFACE_LOST rebuild from TaskStore position x732 y128 rebuildCount1 rebuildCost29ms listenerCount1 statusRECOVERED这套日志能明确区分任务策略问题和窗口恢复问题十二、运行图第一次出现两个任务最终运行图任务列表upload_release_02 61% RUNNING_BACKGROUND compress_assets_01 88% SUSPENDED_POLICY当前 Float ViewactiveJob: upload_release_02异常恢复FLOAT_SURFACE_LOST → rebuild → RECOVERED位置732 / 128所有数据与 DevEco、日志保持一致。十三、为什么本地压缩不偷偷放 Worker 就宣称“后台无限继续”HarmonyOS 当前并发指导确实建议长耗时、常驻计算把工作放到 Worker避免阻塞 UI 主线程。但Worker解决的是不要阻塞 UI不是应用进后台后系统一定允许无限执行后台生命周期和系统调度仍然要遵守 Background Tasks Kit 的规则。所以 QuickDock 把两个问题分开线程执行模型 和 后台运行资格这条边界对技术文章很重要否则很容易把“子线程”误写成“后台保活”。十四、多任务切换也不能复制任务对象现在 Registry 有两个 Task。Float View 切换显示时只改变activeJobId不会把任务内容复制进 WindowState。否则TaskRegistry 61% WindowState 58%又会重回第二篇解决过的状态分裂。所以多任务以后仍然保持TaskRegistry 业务事实源 Window 当前 activeJob 的投影十五、应用回前台后恢复顺序也必须固定前台恢复流程onForeground → 重新评估 BackgroundTaskPolicy → compress_assets_01 SUSPENDED_POLICY → RUNNING → upload_release_02 继续 RUNNING → 检查 Float Surface → 如果已丢失 rebuild → 恢复 activeJob不能一回前台就先新建窗口。因为窗口真正需要展示的是评估后的最新 TaskRegistry。十六、后台任务失败和 Float Surface 丢失是两类异常这一篇还专门区分TASK_FAILED与FLOAT_SURFACE_LOST前者任务业务失败 → Store FAILED → UI 显示失败后者任务仍正常 → 只重建窗口如果把两者都叫ERROR后面根本无法判断到底要不要重启任务。十七、RECOVERED 代表什么最终状态RECOVERED至少代表后台数据任务继续 本地计算遵守策略挂起 两个任务状态没有丢 Float Surface 可以重建 位置恢复正确 listener 仍然只有 1不只是“窗口又出现了”。十八、第四篇最后做了六组压力测试第一组进入后台 107 秒上传继续、压缩挂起。第二组回前台以后压缩恢复不重新生成 taskId。第三组切换 activeJob 三次进度分别保持。第四组模拟一次FLOAT_SURFACE_LOST只重建 UI。第五组重建后位置仍是 732 / 128。第六组恢复后 listenerCount1没有重复订阅。六组通过以后本轮才记成RECOVERED十九、下一轮 05 会把“偶发异常”升级成资源治理做到 04QuickDock 已经拥有Float View Floating Ball Drag Edge Stow Preferences Background Task Multi Task Recovery能力开始多起来以后新的问题会变成重复创建 重复 listener Window 没释放 Ball 没释放 Timer 没释放 TaskRegistry 任务已经结束但 UI 还在05 会专门做生命周期冲突和资源治理。06 最后再跑 25 轮完整回归检查窗口实例 listener timer 内存 切换耗时 任务状态不再加新功能。二十、任务 Registry 必须保存“为什么被挂起”不能只有一个 PAUSEDcompress_assets_01在后台变成SUSPENDED_POLICY我没有写成普通PAUSED因为这两个语义不同。用户点击暂停PAUSED_BY_USER系统策略不允许当前场景继续SUSPENDED_POLICY两者恢复条件也不同。用户暂停的任务不能因为回前台就自动恢复策略挂起的任务则可以在环境允许后恢复。所以最终状态枚举更细exporttypeRegistryTaskStateRUNNING|RUNNING_BACKGROUND|PAUSED_BY_USER|SUSPENDED_POLICY|COMPLETED|FAILED04 当前压缩任务明确是SUSPENDED_POLICY而不是用户主动暂停。这条区别会继续影响 05 的生命周期治理。二十一、后台任务申请失败时要回退而不是伪装成 RUNNING_BACKGROUND数据上传任务计划申请DATA_TRANSFER但系统能力申请仍然可能失败。比如参数不合法 系统调度拒绝 能力不可用这时绝对不能UI 仍显示 RUNNING_BACKGROUNDQuickDock 的处理是request background capability → success → RUNNING_BACKGROUND request failed → BACKGROUND_DENIED → 根据业务决定暂停或回主应用UI 会显示明确的“后台能力不可用”而不是继续画一个假的 61% 进度。当前本轮是成功路径所以最终是RUNNING_BACKGROUND但失败分支已经存在。二十二、activeJob 切换不能改变 TaskRegistry 的排序和生命周期闪控窗当前只显示一个任务。从compress_assets_01切到upload_release_02只是改变activeJobId不会暂停原任务 重排 Registry 重建 Task Runner因此switchCount3只表示显示焦点切换次数。这和“任务切换”这个词很容易混淆。更准确地说QuickDock 切换的是“当前展示任务”不是“系统只允许一条任务活着”。多任务模型如果没有这条边界用户每点一次任务卡片就可能意外影响业务运行。二十三、Float Surface 异常恢复还要防止重复 rebuild如果FLOAT_SURFACE_LOST连续上报两次第一次 rebuild 还没完成第二次又进来会出现rebuildCount2甚至重复订阅 listener。所以 Coordinator 增加恢复锁exportclassFloatRecoveryCoordinator{privaterebuilding:booleanfalseasyncrebuild():Promisevoid{if(this.rebuilding){return}this.rebuildingtruetry{constsnapshotTaskRegistry.shared().activeSnapshot()constposWindowSessionStore.shared().lastPosition()awaitFloatViewAdapterFactory.current().rebuildFromSnapshot(snapshot,pos)}finally{this.rebuildingfalse}}}当前最终rebuildCount1不是因为系统只触发了一次而是 Manager 保证一次恢复流程只有一份。二十四、回前台时先恢复业务状态再恢复 UI应用重新进入前台以后最自然的写法是onForeground → show Float View → resume tasks但这样窗口第一帧可能显示旧状态。QuickDock 改成onForeground → 重新评估 task policy → 更新 TaskRegistry → 恢复 activeJob → 再检查 / 重建 Float View所以窗口看到的第一帧就是最新状态。例如本地压缩SUSPENDED_POLICY → RUNNING如果 Float View 当前 activeJob 切回它页面会直接显示 RUNNING而不是先闪一下 SUSPENDED。二十五、UIAbility 生命周期和 Background Task 生命周期不能互相替代Stage 生命周期提供onForeground onBackground它告诉应用前后台环境变化了Background Tasks Kit 解决的是某类业务是否可以在后台受约束地继续这两个能力层级不同。所以onBackground并不等于开始一个后台任务而只是触发 Policy 评估。同样onForeground也不应该直接把所有任务恢复 RUNNING。只有状态和策略都允许的任务才恢复。这种分层能避免生命周期回调里堆一大坨业务分支。二十六、异常恢复以后要检查旧 Adapter 有没有真正释放FLOAT_SURFACE_LOST重建新显示层后如果旧 Adapter 还持有timer listener surface reference即使用户只看到一个窗口资源也已经重复。所以恢复完成后的检查不仅是listenerCount1还包括activeAdapterCount1当前 04 先记录 listenerCount。05 会把Adapter Window Ball Timer Listener全部纳入统一资源 Registry。第四篇先把异常重建的边界写清楚为下一篇治理做准备。二十七、多任务后台场景最后做了八组验收第一组两个任务同时存在Registry 数量2。第二组进入后台上传进入 RUNNING_BACKGROUND。第三组本地压缩进入 SUSPENDED_POLICY。第四组后台 107 秒后上传从 36% 到 61%。第五组回前台压缩从 88% 继续不生成新 taskId。第六组activeJob 切换 3 次两个任务进度各自保持。第七组模拟 FLOAT_SURFACE_LOSTrebuildCount1。第八组重建后listenerCount1 position732/128 statusRECOVERED所有条件都满足04 才正式结束。二十八、这一篇最后留下的是“后台策略”和“显示恢复”两条独立链路回头看第四篇最重要的不是窗口异常本身。而是把两条链彻底分开后台策略链 Stage → BackgroundTaskPolicy → TaskRegistry 显示恢复链 FLOAT_SURFACE_LOST → FloatRecoveryCoordinator → Adapter rebuild两条链最后都读同一个 TaskRegistry但互相不替代。这让 QuickDock 后面即使换成下载 上传 视频导出 模型处理仍然可以复用相同的窗口恢复架构。参考资料HarmonyOS 7 闪控窗开发指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/float-view-guideBackground Tasks Kithttps://developer.huawei.com/consumer/cn/doc/常驻任务并发场景https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/resident-task-overview后台视频导出最佳实践https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-video-background-export