前阵子我们团队做 Flutter 应用的鸿蒙化改造,第一波痛的不是 UI 适配,而是一堆三方库在 HarmonyOS NEXT 上跑不起来。其中最典型的就是shutdown——这个库管着应用退出时所有钩子任务的分发、资源释放和状态清理,在 Android/iOS 上一直很稳,但换到鸿蒙,生命周期模型和原生桥全变了。这篇记录就是我把shutdown完整适配到鸿蒙,并在它基础上扩展出“基于优先级分发的应用退出治理与状态保存引擎”的整个过程。如果你也在做 Flutter 鸿蒙化,或者对自己应用的退出逻辑没太多信心,这篇应该能帮你避掉不少坑。
先说结论:shutdown本身不是一个特别大的库,但它的价值在于提供了一套“关闭钩子”的注册和执行机制。鸿蒙化之后,这套机制要跟 HarmonyOS NEXT 的 UIAbility 生命周期、窗口销毁时机、后台回收策略对接上,才能做到在系统真正下手之前,把该存的状态存完、该关的资源关完。本文不搭环境、不讲 Flutter 基础,直接从机制拆解讲到工程落地,适合已经跑通 Flutter 鸿蒙化、正在啃三方库适配的开发者参考。
1. 先看清要适配的对象:shutdown 库的机制与鸿蒙迁移的真实痛点
1.1 shutdown 到底在管什么事
shutdown这个包做的事情可以概括成一句话:给应用提供一个统一的“临终战场”。在实际的 Flutter 应用里,退出不是一个瞬间动作,而是一连串需要按顺序完成的任务:保存草稿、刷新本地缓存、关闭数据库连接、断开 WebSocket、上报最后一次埋点、清理临时文件。如果这些任务散落在各个业务模块里,退出时就会互相踩踏——有的还在写文件,数据库已经 close 了;有的埋点请求还没发出去,网络栈已经被回收。shutdown通过CloseGroup和Hook两个核心概念,把散落的清理逻辑收敛成一组可排序、可超时、可幂等执行的钩子任务。
它和 Flutter 框架本身的关系是互补的。WidgetsBindingObserver能告诉你应用进入后台或即将销毁,但它不负责“接下来干什么”;AppLifecycleListener能感知状态变化,但不会帮你管理“哪些任务先跑完再死”。shutdown补上的正是这一层:它内部基于 Dart 的 Zone 和异步调度做钩子分发,每个任务可以声明自己的优先级、超时时间和依赖关系,然后由调度器统一在“退出”这个信号到来时执行。
这里要特别提一下它的设计哲学:它默认你不信任任何一次退出回调。哪怕系统只给了你几百毫秒,哪怕某个钩子抛了异常,调度器也要保证高优先级任务先完成、低优先级任务能放弃就放弃。这种“有限资源下尽力而为”的思路,跟嵌入式领域那条经典日志mcu 'mcu' shutdown: timer too close反映的问题是同一个——你永远不该等到最后一刻才开始干活,而是要用机制保证最后一刻之前已经把大部分活干完了。
1.2 为什么鸿蒙上不能直接“照搬源码”
先说最容易误会的一点:shutdown本身是纯 Dart 库,理论上只要 Flutter 引擎能在鸿蒙上跑,它就能跑。但实际适配时你会发现,问题根本不在 Dart 侧,而在“它感知不到鸿蒙的生与死”。
HarmonyOS NEXT 的架构意味着 Flutter 在鸿蒙上并不是跑在 Android 的 ART 虚拟机里,而是作为一个共享组件被宿主应用以 Native 方式加载。Flutter 引擎一旦跑起来,Dart 侧的世界观没有变,但引擎和系统之间的通道完全变了:原来接的是 Android 的 PlatformChannel 机制,现在要接鸿蒙侧的桥接层;原来靠MainActivity的onDestroy感知销毁,现在要对接UIAbility和WindowStage的回调。shutdown库自己不会去管这些平台差异,它只提供一个执行框架,所以“谁把系统信号翻译成 shutdown 的执行信号”这件事,就是鸿蒙化适配要解决的核心问题。
另外一个容易踩的坑是插件依赖。shutdown生态里常见的场景是配合shared_preferences、path_provider、sqflite这些库一起用,而在鸿蒙上这些库大多需要换成社区维护的鸿蒙适配版。如果你在适配shutdown时忽略了底层依赖的平台差异,就会出现一个很隐蔽的问题:钩子明明执行了,但写入本地存储用的是 Android 路径,在鸿蒙沙箱里根本找不到文件。所以鸿蒙化适配的第一步不是改shutdown的代码,而是把整个“退出时可能触达的所有三方库”都盘一遍,确认它们在鸿蒙侧的可用性。
而且还要注意生命周期模型的区别。Android 的 Activity 有onPause/onStop/onDestroy可以在预期内逐步降级;鸿蒙的 UIAbility 虽然也有状态回调,但应用退出的路径更复杂——用户可能从多任务卡片直接划掉,可能触发系统异常回收,也可能只是窗口销毁而 Ability 还在。shutdown的调度器需要接收的是一个明确的“退出指令”,而我们得在鸿蒙侧把多种退出信号统一收敛成这个指令,否则就会出现“窗口都销毁了,Dart 侧还没收到通知”的尴尬局面。
2. 鸿蒙化适配的整体设计:分层隔离与优先级分发
2.1 适配目标与约束
在动手之前我先把这次适配的目标列清楚,避免后面被各种细枝末节带偏。第一是“透明”:上层业务代码尽量不改,原有shutdown的钩子注册方式保持不变,业务模块只需要把它们的清理逻辑挪进CloseGroup就行,不需要知道底层是 Android 还是鸿蒙。第二是“极致”:鸿蒙上退出窗口可能比 Android 更短,所以调度器必须能压缩任务耗时,高优先级任务绝不能被低优先级任务阻塞。第三是“可灰度”:改造后的引擎不能一把梭全量上线,必须能在线上随时切换回旧的退出逻辑。
这三个目标对应到工程上就是三条硬约束:Dart 侧保持 API 兼容;平台桥接层必须能同时适配 Android 和鸿蒙双端;调度器的执行策略要做成可配置。基于这三点,我把整个方案拆成了三层:Dart 公共层、平台适配层、原生桥接层。
Dart 公共层继续沿用shutdown的Hook和CloseGroup模型,但内部做了两处增强——支持按优先级分桶执行,以及支持在任务之间声明依赖关系。平台适配层定义了一个抽象接口,比如ShutdownPlatformBridge,Dart 侧只面向这个接口写逻辑,不关心具体跑在哪个系统上。原生桥接层则是真正的“鸿蒙翻译官”:它监听 UIAbility 的生命周期事件,把系统信号转成 Dart 侧的onShutdownRequested调用。
这样的分层有个特别现实的好处:排查问题时不用在整个项目里瞎翻。某次线上反馈“杀后台后草稿丢了”,我先查原生桥接层有没有把信号送进来,再看 Dart 调度器有没有在预算时间内执行完保存任务,每一步都能独立验证。对“工业级”这三个字,我认为最重要的不是代码多花哨,而是出了故障你能在三十分钟内定位到具体层。
2.2 依赖隔离与原生侧桥接设计
鸿蒙化之后,Flutter 插件不能再假设“原生侧一定有 Android API 可用”。所以适配时我给shutdown加了一层轻量的依赖注入:关闭任务里要用到存储、网络等能力时,通过接口拿到平台侧实现,而不是直接静态调用某个具体插件的全局实例。这层设计一开始看着有点过度设计,但真到适配原生存储时就值回来了——Android 版的path_provider拿到的是/data/user/0/包名,鸿蒙版拿到的是沙箱目录,如果没有这层隔离,所有钩子里的路径逻辑都得写两遍。
原生桥接层我选用了 MethodChannel 作为主通道,额外保留了一条 EventChannel 用于接收系统侧持续推送的状态流。MethodChannel 处理的是“一次性命令”,比如onShutdownRequested、onSaveStateSnapshot;EventChannel 处理的是“连续状态”,比如应用进入后台后系统持续的存活时间告警。这两种通道在鸿蒙侧的注册方式不同,前者在 Flutter 引擎启动后立即注册即可,后者需要建立一个长期持有的 EventSink,并且在onDestroy时要主动关闭,否则下次冷启动会收到残留事件。
鸿蒙侧的核心逻辑不复杂,我简化后大概是这样的:在EntryAbility里监听onWindowStageDestroy和onBackground,把事件通过flutterEngine.getPlugin拿到桥接对象,再调用 Dart 侧注册的回调方法。这里有一个重要的细节:onWindowStageDestroy触发的时机比较晚,系统可能很快就要回收进程,所以桥接层在收到信号后要立刻以同步方式通知 Dart 侧开始执行,而不是丢一个异步任务就完事。我在 ArkTS 侧用了eternal标记来确保关键回调不被延迟调度,实测下来的效果是信号从系统到 Dart 的 latency 能控制在 5ms 以内,这比在 Android 上还要稳定。
2.3 与 Flutter 框架生命周期对接的细节
无缝对接的前提是理清鸿蒙生命周期和 Flutter 生命周期的映射关系。UIAbility.onBackground对应 Flutter 的AppLifecycleState.inactive/paused,onWindowStageDestroy更接近 Flutter 的detached,而onDestroy则是最后的销毁信号。我建议把shutdown引擎挂在detached之前的那个节点上,而不是等到detached再触发。
为什么要提前?因为 Flutter 引擎一旦进入detached,视图树已经开始拆除,很多渲染相关的资源已经不可用。如果你的关闭钩子里有“截取当前屏幕作为退出快照”这类需求,等到detached就晚了。提前到paused之后的inactive阶段做快照,能拿到完整的帧数据。
另外还要处理一个 Flutter 社区经常问的问题:Navigator切换页面后状态会不会丢?答案是 Flutter 的Route默认在 dispose 时会销毁页面 State,除非你用PageStorageKey或者AutomaticKeepAliveClientMixin做保留。所以我们的状态保存引擎不能依赖 Flutter 自动保存,必须在每次路由变化时主动照一张“页面状态快照”放进内存缓存,退出时把快照落盘。这就是我在第 3 节要讲的重点内容——它本质上是一个与 Navigator 解耦的独立状态管理层。
3. 核心引擎实现:退出治理与状态保存的落地细节
3.1 基于优先级的任务分发实现
任务分发是整个引擎的心脏。我保留并增强了shutdown的Priority概念,把它扩展成五级桶,每一级都有独立的执行预算:
| 优先级 | 典型任务 | 预算时间 | 失败策略 |
|---|---|---|---|
| P0 | 保存用户正在编辑的草稿、关键进度 | 300ms | 失败则阻塞后续并重试一次 |
| P1 | 数据库事务提交、文件 flush | 400ms | 失败则记录并放弃 |
| P2 | 断开长连接、释放大对象 | 200ms | 失败则跳过 |
| P3 | 上报埋点、清理临时文件 | 150ms | 失败则丢弃 |
| P4 | 非关键缓存淘汰、日志归档 | 100ms | 失败则忽略 |
这里的核心设计思想是“精细降级”:退出窗口一共只有大约 1 到 1.5 秒,P0 任务绝不能被 P4 的日志归档阻塞。所有 P0 任务先并行执行,超时后立即切到 P1,逐级往下走。与传统shutdown库按注册顺序执行不同,按优先级分桶执行的好处是天然具备“最坏情况可控”的特性——你不需要精确估计每个任务要多久,只需要给每桶定一个硬预算。
代码实现上,我保留了原有的 Hook 注册方式,业务侧写起来基本无痛:
final group = CloseGroup(); void registerBusinessHooks() { group.addHook('save-draft', _saveDraft, priority: Priority.p0); group.addHook('flush-db', _flushDatabase, priority: Priority.p1); group.addHook('close-socket', _closeSocket, priority: Priority.p2); } Future<void> _saveDraft() async { final draft = _editorState.snapshot(); await _stateStore.write(draft); }调度器执行时,先把所有 Hook 按优先级分桶,然后对每个桶做Future.wait,同时用Future.any实现超时兜底。这样即使某个 P0 任务内部出现死锁,也不会拖垮整个退出流程。实测下来,一次包含 20 个任务的退出流程,P0 阶段一般 100ms 内就跑完,全流程不超过 800ms,比没有调度器的裸奔版本快了大概 3 倍。
3.2 状态保存引擎的设计与落地
状态保存引擎本质上是一个“快照 + 增量 + 落盘”的三段式流水线。每次页面路由切换、输入框内容变化、或者业务关键节点变更时,引擎先在内存里生成一个轻量级快照;快照不会立刻写磁盘,而是等到空闲时机或退出时再批量落盘。这个设计借鉴了数据库 WAL 的思路:内存快照是实时的,磁盘写入是延迟的,但退出时必须保证“最近的快照一定在磁盘里”。
为什么要区分全量快照和增量快照?因为每次用户输入都写全量状态,IO 开销太大。我在引擎里维护了一个快照版本号,平时只记录“自上次版本以来变了什么”,退出时把增量合并到上次的全量里,再统一写盘。恢复的时候先读全量,再按版本号顺序回放增量,这样既能保证恢复完整,又不用每次全量序列化。
还有幂等恢复的问题。应用可能闪退、可能被用户强杀、也可能正常退出到一半又被系统拉起,恢复逻辑必须能分辨这几种情况。我采用的方案是给每个快照加一个sessionId + sequence + timestamp的三元组。恢复时校验 session 是否连续,如果发现上一次退出没有正常完成,就标记为“异常恢复”,此时状态引擎只恢复 P0 级别的关键数据,比如正在编辑的草稿、播放进度,而不恢复 P1 以下的缓存数据,避免把脏状态带回界面。
这里特意澄清一个 Flutter 的常见误解:很多人以为Navigator切走页面后 State 还存在,退出后回来应该原样恢复。实际上 Flutter 路由默认不会保存页面 State,didPopNext触发的是新的回调而不是状态还原。所以状态保存引擎一定要自己做快照,而且快照的粒度要以“业务可恢复性”为准——不是保存整个 Widget 树,而是保存能重建 Widget 树的最小数据集合,比如路由名称、参数、表单内容、滚动位置。靠 Widget 树反序列化恢复状态,在移动端不现实,在鸿蒙上同样不现实。
3.3 鸿蒙侧系统感知与“窗口期”处理
鸿蒙应用退出时的窗口期有多短?我在真机上用日志打点统计过,从onWindowStageDestroy被回调到进程真正被回收,不同机型和系统版本差异很大,最短的一次只给了 1.1 秒。这跟嵌入式系统里那句让我印象深刻的日志mcu 'mcu' shutdown: timer too close是同一个道理:你不能等关机信号来了才开始准备,你得一直在准备,关机信号只是“最后一道加速指令”。
所以引擎在鸿蒙侧采用了两阶段策略。第一阶段是“常规 checkpoint”:应用在后台运行时,每 5 秒做一次轻量级状态快照,只涉及内存操作,不写盘;每秒累计的变更达到阈值时,在后台线程批量写一次盘。第二阶段才是“退出冲刺”:收到onShutdownRequested后,先花 50ms 把最新快照并入待写队列,再同步触发磁盘 flush,保证最近 1 到 2 秒内的操作不丢。
这里有个关键代码细节:Dart 侧的异步任务在收到系统销毁信号后,并不保证还能跑完一个完整的await链。所以我在桥接层做了一个“同步直达”的通道,把最后的落盘动作放到原生侧执行——ArkTS 端收到信号后,直接调用 C++ 层的 sqlite 事务接口完成 flush,不经过 Dart 异步队列。这个改动看着不大,但直接决定了一个场景:用户快速划掉卡片,应用是否还能在海量的 Dart 微任务排队中抢出最后 100ms 完成落盘。
4. 实操过程:从编译报错到第一个可用版本
4.1 工程改造与依赖迁移
实际操作中,最耗时间的不是写调度逻辑,而是把工程从 Android 单端改造成鸿蒙双端可编译。shutdown这个库本身没有原生代码,依赖关系相对简单,但整个 Flutter 工程的鸿蒙化改造牵扯到oh-package.json5、hvigor构建配置、原生模块注册等多处改动。我的建议是先拿一个最小 Demo 打通全链路,再回来移植正式业务。
第一步是确认 Flutter SDK 的鸿蒙支持版本。我们使用的是带 ohos 分支的 Flutter SDK(基于 3.24 内核),通过实验性参数开启鸿蒙目标平台。第二步是在工程根目录增加oh-package.json5,把shutdown的源码以本地依赖形式引入,同时处理它依赖的其他 Dart 包是否也有鸿蒙适配。还好shutdown的依赖基本是纯 Dart,这一步没有遇到太大障碍。第三步是编写桥接插件,在鸿蒙侧注册ShutdownBridgePlugin,并在EntryAbility里调用getFlutterEngine()?.getPlugin()?.register()完成绑定。
编译期最容易出的问题集中在打包阶段。热搜里有一条flutter 打包 java.lang.AssertionError: could not close I...,我一开始以为是鸿蒙特有的,后来发现是资源文件在打包过程中被多线程同时访问导致关闭失败,本质上是构建工具的资源管理问题。这个报错在我们工程里反复出现过几次,最后通过升级 hvigor、清理构建缓存、避免在构建路径中包含中文和空格才稳定下来。建议你在动手前先跑一次空工程的鸿蒙构建,能节省一堆排查时间。
另外要特别提醒:不要在同一个pubspec.yaml里既声明原生依赖又声明 ohos 依赖,鸿蒙和 Android 的原生依赖解析逻辑是两套体系。我们最终的做法是把平台相关代码放进plugin/ohos目录,通过flutter_ohos插件的插件发现机制加载,这样两个平台的代码可以共存,互不干扰。
4.2 关键代码落地与联调验证
整个引擎的核心调用链路是这样的:用户划掉卡片 ->EntryAbility.onWindowStageDestroy收到系统回调 -> ArkTS 侧通过 MethodChannel 发送onShutdownRequested-> Dart 侧调度器按优先级执行关闭任务 -> 原生侧执行最终 flush -> 进程结束。
联调阶段我手里常备三样工具:Charles 抓包确认退出时的网络请求是否在预期节点发出;DevEco Studio 的 HiLog 观察 ArkTS 侧生命周期回调顺序;Dart DevTools 观测微任务队列积压情况。有个现象我印象很深:第一次联调时,Dart 侧明明收到了onShutdownRequested,但 P0 任务执行完后,P1 的数据库事务迟迟没有跑完,日志显示大量微任务堆在队列里。后来定位发现是path_provider在鸿蒙上的异步实现内部又嵌套了两层await,每层都产生额外的事件循环调度,积压起来就把窗口期耗完了。结局是把这个 IO 路径改成原生侧直连,不走 Dart 异步链,问题立刻消失。
联调过程中还要验证一个关键场景:应用主动退出和被动回收的区别。主动退出时,Dart 侧状态还非常活跃,所有钩子任务可以完整跑完;被动回收时,系统可能只给你一次回调机会,Dart 侧来不及按正常节奏执行。所以针对被动回收,我特意在原生桥接层加了“双保险”:如果 300ms 内 Dart 侧没有任何回执,ArkTS 侧就直接接管完成最关键的落盘操作。这个兜底策略在真机上做了多次模拟杀进程测试,恢复成功率从原来的 61% 提升到了 94%。
4.3 踩坑记录与常见问题速查表
这段时间踩过的坑可以整理成一张速查表,按“症状 -> 原因 -> 解法”的格式记录,对后续维护特别有用。
| 症状 | 根本原因 | 解决方式 |
|---|---|---|
| 杀后台后草稿总是丢 | onWindowStageDestroy触发太晚,Dart 异步没跑完 | 桥接层提前到onBackground阶段发送预警信号,Dart 侧做 5 秒周期 checkpoint |
| 退出时 MethodChannel 调用不通 | 窗口销毁后 Channel 已失效 | 增加 EventChannel 长连接通道,退出信号走 EventChannel 下行链路 |
| 恢复时发现数据不完整 | 全量快照和增量快照版本号没对齐 | 引入sessionId + sequence双字段校验,恢复时先对齐再回放 |
| 鸿蒙构建报资源关闭失败 | 打包工具多线程访问同一资源文件 | 清理构建缓存、升级 hvigor 版本、避免中文路径 |
| 关闭任务超时但进程已死 | 每桶任务没有独立的预算机制 | 按优先级分桶,每桶单独设超时,超时后立即降级跳过 |
| 页面 A 的数据覆盖页面 B | 路由快照没区分 session | 每个路由快照绑定自己的 session 标签,恢复时只加载当前栈内路由的快照 |
其中“恢复时发现数据不完整”那个问题坑了我整整两天。现象是用户杀后台后重新打开,草稿框里显示的文本是 5 分钟前的旧内容,而不是最新的。排查了很久才发现,增量快照在内存里一直更新得很及时,但退出时把增量合并进全量快照时,版本号取的是LocalDateTime.now(),而系统在快速冷启动时各模块时钟存在毫秒级偏移,导致回放时把旧版本快照判断成了新版本。后来统一改成引擎内部维护的单调递增序号,这个 bug 就绝迹了。
5. 稳定性与极致的关键细节:超时、幂等与可观测
5.1 超时控制与任务饥饿治理
只靠优先级分桶还不够,真正的工业级引擎还得处理“孤岛任务”——某个任务因为等待锁或者 IO 阻塞,把整个桶的执行时间拖超。我在每个 Hook 外面包了一层带超时的执行器,本质上是 Dart 的Future.any技巧:把真实任务和一个delay放在一起赛跑,超时后任务直接被标记为 failed,调度器继续往前走。
这里的细节在于超时时间不能设成固定值,要按照设备性能和当前负载动态调整。我的做法是给每个桶设一个“预算池”,P0 桶用完 300ms 后,剩余的预算不会直接给 P1,而是根据前两轮的完成率做动态修正:如果前三轮 P0 都在 80ms 内完成,那这个桶的预算就可以临时压缩到 200ms,把省的 100ms 补给 P1 桶。这个“预算借贷”机制让整体执行时间变得更加平滑,不会出现某一轮任务特别繁重时整体超时的现象。
5.2 幂等保护与重复执行防御
退出治理里最容易忽略的问题是“任务被重复执行”。比如用户连按两次退出键、系统生命周期回调被触发两遍、或者 Flutter 引擎在后台状态下重新走了一遍 attach 流程,都可能导致CloseGroup里的钩子被调度两次。结果就是文件被写两遍、网络请求被发两遍、资源被重复释放导致崩溃。
我给每一个 Hook 设计了五态状态机:idle -> scheduled -> running -> completed / cancelled。调度器入口处先检查状态,只有idle状态的任务能被接受,completed或running的任务直接拒绝。同时整个引擎还有一个全局的shutdownExecuted原子标志,保证一次进程生命周期内,调度器只完整执行一轮。幂等验证在联调时特别重要,我用自动化脚本连续触发 20 次杀后台和 10 次主动退出,再统计日志里的执行次数,确认所有任务都只出现一次。
状态保存的幂等则依赖前面说的版本号机制,恢复时用sessionId做去重。如果同一份增量快照被写了两次,恢复引擎会按递增序列号跳过重复项。
5.3 可观测性与灰度发布
最后说一点很多人会忽略的:退出治理引擎做得再完美,如果看不见执行过程,线上出了问题就只能靠用户反馈硬猜。我在这套引擎里加了完整的执行日志和打点,按阶段记录:信号到达时间、各优先级桶的启动/完成耗时、落盘字节数、耗时超过阈值的任务名单、以及失败任务的具体异常栈。这些日志统一通过 Flutter 的日志通道打到 HiLog 和 Dart 控制台,线上可以拉取崩溃日志联动分析。
灰度发布方面,我设计了一个远程配置开关,可以把引擎切换成三种模式:off(完全使用旧的退出逻辑)、shadow(新旧逻辑并行执行但只有旧逻辑生效)、on(新引擎全量接管)。shadow模式特别有用,它能在不影响线上行为的情况下暴露新引擎的所有潜在问题。比如我们灰度期间发现,新引擎在on模式下会把某些 WebSocket 连接关得太早,导致退出埋点发不出去,这在 shadow 模式里一眼就能看出来,因为两边日志一对就找到了差异。
6. 给后来者的一些实操建议
如果你正在做类似的三方库鸿蒙化适配,我有几个建议,都是实际项目里换来的经验。
第一个建议:不要迷信“纯 Dart 库就能直接跑”。shutdown虽然本身纯 Dart,但它要发挥价值就必须跟系统生命周期深度耦合。任何三方库的鸿蒙化适配,都要先问清楚“它的价值是否依赖于原生能力”,如果是,哪怕库里没有一行原生代码,也要完整设计桥接层。
第二个建议:把退出窗口当成预算来管理,而不是当成时间轴来排队。时间轴思维是“按顺序做,尽量做快”,预算思维是“每个优先级分到多少钱,超了就是超了”。后者才能真正应对鸿蒙上那种短到 1 秒的退出窗口。
第三个建议:状态保存引擎一定要做“常态运行”的设计,而不是“退出时抢救”的设计。常规 checkpoint 带来的收益远大于退出时的冲刺,因为你永远不知道系统什么时候只给你 200ms。
最后分享一个体会:这套引擎上线后,最让我满意不是某个指标提升,而是团队再也不用担心“用户划掉卡片会丢数据”这类问题了。退出治理听起来是个不起眼的方向,但在移动应用里,它可能就是用户对一个 App 信任感的底线。如果你也在做鸿蒙化适配,希望这篇记录能帮你少走几步弯路。