在做另一个功能时,我发现鸿蒙还有一个叫“UIAbility 备份恢复”的机制,乍一看和 appRecovery 挺像的,都是挂了之后恢复数据。但搞了一下午才发现这两个东西完全不是一回事。appRecovery 侧重的是“崩溃时保命”,而 UIAbility 备份恢复侧重的是“后台被杀后保数据”。而且这俩的 API 调用方式、数据存储位置、触发场景都不一样。这篇文章就把 UIAbility 备份恢复的来龙去脉讲清楚,顺便把和 appRecovery 的区别也说透。
什么时候会触发备份恢复
官方文档就几句话,但实际理解起来有点绕。我用自己的话翻译一下。
备份触发条件
应用在后台运行的时候,如果因为下面这些原因被系统干掉,才会触发备份:
- 系统资源管控(内存不够了把你杀了)
- 进程被 kill(比如系统做了进程清理)
- 应用异常崩溃(JS Crash、C++ 异常之类的)
说白了就是“非自愿死亡”才备份。如果你自己主动调 terminateSelf 退出,或者用户在多任务中心滑掉卡片,不触发备份。
恢复触发条件
恢复就更挑场景了。应用被 kill 之后,下次冷启动进来的时候,系统才会自动恢复之前备份的数据。
什么情况不触发恢复?官方说了两条:
- 应用正常关闭(比如用户主动退出),不触发恢复
- 通过 startAbility 或者点图标正常启动的,不触发恢复
等等,这里有个让人懵的地方——应用被杀了之后重新启动,不就是冷启动吗?这难道不是点图标启动?对,确实是的。所以实际逻辑是这样的:应用被系统杀掉了 -> 用户再次点击图标打开应用 -> 此时是冷启动,但因为“正常启动不触发恢复”,你可能会觉得恢复没生效。
关键点:系统判定“是否恢复”的依据不是启动方式,而是上次退出是否备份了数据。如果 onSaveState 被调用了且保存了数据,下次冷启动不管什么方式进来的,系统都会把数据塞到 want.parameters 里。但页面栈的恢复只在特定场景下生效——比如通过任务列表恢复。这个区别后面讲运行机制的时候再细说。
踩坑:别以为开启了备份恢复就万事大吉。我一开始以为只要调了 setRestoreEnabled(true),应用被杀后重启就能恢复到之前的页面。结果测试发现页面回到了首页,查了半天文档才发现页面栈恢复是有条件的——它依赖任务保留机制。如果你的应用设置了 removeMissionAfterTerminate: true,备份恢复直接不生效。
运行机制
数据怎么备份的
当应用在后台被系统杀掉时,系统会触发 UIAbility 的 onSaveState 回调。你在这个回调里把需要保存的数据塞到 wantParam 这个 Record<string, Object> 里,系统会帮你序列化存到沙箱里。签名长这样:- onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult
复制代码 参数说明:
- reason:备份的原因,目前主要是 StateType.APP_LOW_MEMORY 之类的
- wantParam:你要保存的数据,键值对往里塞就行
- 返回值:返回 OnSaveResult.ALL_AGREE 表示同意备份
这里有个细节:wantParam 最终会被存储到 Want 的 parameters 字段里,所以恢复的时候是从 want.parameters 读的。
数据怎么恢复的
恢复分两部分:
- 业务数据:在 onCreate 生命周期里,从 want.parameters 中读取你之前保存的数据。因为冷启动的时候系统会把之前备份的 Want 传进来。
- 页面栈:在 onWindowStageCreate 生命周期中恢复。系统会自动恢复之前保存的页面栈信息,所以你不需要手动做任何操作——前提是条件满足(没有设置 removeMissionAfterTerminate、设备支持任务保留等)。
不过说实话,页面栈恢复这个东西效果有限。我之前测试的时候发现它只能恢复页面导航栈,但页面内部的状态(比如滚动位置、输入框内容、Tab 选中状态)并不会自动恢复。这些需要你自己在 onSaveState 里保存,然后在页面初始化时手动恢复。
完整的生命周期流程:
正常情况:- 冷启动 -> onCreate -> onWindowStageCreate -> onForeground -> 用户操作 -> onBackground -> 被杀
复制代码 有备份恢复的情况下:- 被杀 (触发 onSaveState) -> ...
- 下次冷启动 -> onCreate(从 want.parameters 读数据) -> onWindowStageCreate(恢复页面栈) -> onForeground
复制代码
约束限制
这块不仔细看文档的话很容易踩坑,我列几个重要的。
数据大小限制:200KB
备份数据存储在 Want 的 parameters 里,序列化之后的体积不能超过 200KB。200KB 是什么概念?如果你存一屏幕的文本数据可能还行,但如果你想把一张 Base64 图片塞进去——别想了,肯定超。我当时写了个 demo,把用户编辑的富文本内容(包含多张图片的 Base64)存到 wantParam 里,结果一跑就发现数据没恢复。排查了半天,日志里也没报错。后来才发现是超限了,系统默默地丢弃了数据。
保存期限:7 天
备份数据是以文件形式存在应用的沙箱路径里的,保留 7 天。7 天后系统会自动清理。这意味着如果你用户 7 天没打开你的应用,回来之后数据就没了。不过这也很合理——7 天都没打开的 app,估计用户早就不 care 那点数据了。
重启设备不支持还原
这一点真的坑。如果你的手机重启了,之前的备份数据就全没了。我一开始不理解,后来想到可能是备份数据存在内存文件系统里,重启就丢了。但官方没有明确说存在哪里,只说了不支持重启设备后还原。经验之谈:如果你的应用有非常重要的数据,别依赖这个备份恢复机制。它更适合保存“没了会有点麻烦但也不是不能忍”的临时状态,比如用户正在编辑的表单内容、上次浏览的位置等。真正的关键数据还是得自己做持久化。
removeMissionAfterTerminate 冲突
这个前面提到了,如果你的 module.json5 里设置了 removeMissionAfterTerminate: true,备份恢复机制不生效。为什么呢?因为备份恢复依赖任务保留机制(mission),如果 Ability 退出时就把任务记录删了,系统就无从恢复。所以如果你同时用了备份恢复和 removeMissionAfterTerminate,要注意这两个功能是互斥的。- // module.json5 - 错误配置
- {
- "abilities": [
- {
- "name": "EntryAbility",
- "removeMissionAfterTerminate": true, // 这个会禁用备份恢复
- // ...
- }
- ]
- }
复制代码
UIExtensionAbility 不支持
这个好理解,UIExtensionAbility 没有自己的页面栈,本身就是附属于其他 Ability 的,所以不支持备份恢复。
接口说明
备份恢复的核心接口就一个:- setRestoreEnabled(enabled: boolean): void
复制代码 这个方法是 UIAbilityContext 提供的,从 API 14 开始支持。调用时机要求:必须在 onForeground 之前调用,一般放在 onCreate 里。- import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
- export default class EntryAbility extends UIAbility {
- onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- // 必须在 onForeground 前调用
- this.context.setRestoreEnabled(true);
- // 其他初始化...
- }
- }
复制代码 接口就这么简单,一个 boolean 参数,开了就是开了,关了就是关了。
开发步骤
第一步:启用备份恢复
在 EntryAbility 的 onCreate 里调用 setRestoreEnabled(true):- // entry/src/main/ets/entryability/EntryAbility.ets
- import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
- import { hilog } from '@kit.PerformanceAnalysisKit';
- const DOMAIN = 0x0000;
- export default class EntryAbility extends UIAbility {
- onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- // 启用 UIAbility 备份恢复
- this.context.setRestoreEnabled(true);
- hilog.info(DOMAIN, 'Demo', 'UIAbility backup restore enabled');
- // 其他初始化...
- }
- }
复制代码
第二步:保存数据
实现 onSaveState 方法,把需要恢复的数据存到 wantParam 里:- // entry/src/main/ets/entryability/EntryAbility.ets
- import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
- import { hilog } from '@kit.PerformanceAnalysisKit';
- const DOMAIN = 0x0000;
- export default class EntryAbility extends UIAbility {
- // ... onCreate ...
- onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
- hilog.info(DOMAIN, 'Demo', 'onSaveState triggered, reason: %{public}s', JSON.stringify(reason));
- // 保存用户正在编辑的数据
- // 假设这些数据来自 AppStorage 或页面传入
- wantParam['editContent'] = AppStorage.get<string>('draftContent') ?? '';
- wantParam['scrollPosition'] = AppStorage.get<number>('scrollPosition') ?? 0;
- wantParam['selectedTab'] = AppStorage.get<number>('selectedTabIndex') ?? 0;
- return AbilityConstant.OnSaveResult.ALL_AGREE;
- }
- }
复制代码 注意这里 reason 的类型是 AbilityConstant.StateType,常见的值包括:StateType.APP_LOW_MEMORY(内存不足被回收)、StateType.WINDOW_VISIBILITY_CHANGE(窗口可见性变化)等。
第三步:恢复数据
在 onCreate 里从 want.parameters 读取备份的数据:- // entry/src/main/ets/entryability/EntryAbility.ets
- import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
- import { hilog } from '@kit.PerformanceAnalysisKit';
- const DOMAIN = 0x0000;
- export default class EntryAbility extends UIAbility {
- onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- this.context.setRestoreEnabled(true);
- // 检查是否有备份恢复数据
- if (want && want.parameters) {
- const editContent = want.parameters['editContent'] as string;
- const scrollPosition = want.parameters['scrollPosition'] as number;
- const selectedTab = want.parameters['selectedTab'] as number;
- if (editContent !== undefined) {
- hilog.info(DOMAIN, 'Demo', '恢复草稿内容');
- // 将恢复的数据存入 AppStorage,页面读取后回显
- AppStorage.setOrCreate('draftContent', editContent);
- AppStorage.setOrCreate('scrollPosition', scrollPosition);
- AppStorage.setOrCreate('selectedTabIndex', selectedTab);
- }
- }
- // 其他初始化...
- }
- // ... onSaveState ...
- }
复制代码
完整示例
把上面三步合起来,就是一个完整的可运行例子:- // entry/src/main/ets/entryability/EntryAbility.ets
- import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
- import { hilog } from '@kit.PerformanceAnalysisKit';
- import { window } from '@kit.ArkUI';
- const DOMAIN = 0x0000;
- export default class EntryAbility extends UIAbility {
- onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- // 1. 启用备份恢复
- this.context.setRestoreEnabled(true);
- hilog.info(DOMAIN, 'Demo', 'UIAbility backup restore enabled');
- // 2. 检查是否有备份数据需要恢复
- if (want && want.parameters) {
- const savedData = want.parameters['myAppState'] as string;
- if (savedData) {
- hilog.info(DOMAIN, 'Demo', '恢复数据: %{public}s', savedData);
- AppStorage.setOrCreate('restoredState', savedData);
- }
- }
- }
- onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
- hilog.info(DOMAIN, 'Demo', 'onSaveState 被触发');
- // 保存应用当前状态
- wantParam['myAppState'] = JSON.stringify({
- lastPage: AppStorage.get<string>('currentPage'),
- editText: AppStorage.get<string>('editText'),
- timestamp: Date.now()
- });
- return AbilityConstant.OnSaveResult.ALL_AGREE;
- }
- onWindowStageCreate(windowStage: window.WindowStage): void {
- // 系统会自动恢复页面栈,这里正常加载页面就行
- windowStage.loadContent('pages/Index', (err) => {
- if (err.code) {
- hilog.error(DOMAIN, 'Demo', '加载页面失败: %{public}s', JSON.stringify(err));
- return;
- }
- hilog.info(DOMAIN, 'Demo', '首页加载成功');
- });
- }
- }
复制代码
如何在页面中使用恢复的数据
备份恢复的核心在 Ability 层,但你恢复的数据最终是要给页面用的。这里说一个常见的做法:通过 AppStorage 桥接。在 Ability 的 onCreate 里把数据写入 AppStorage,然后在页面中通过 @StorageLink 或 AppStorage.get 读取:- // pages/EditorPage.ets
- @Entry
- @Component
- struct EditorPage {
- @StorageLink('restoredState') restoredState: string = '';
- @State editContent: string = '';
- aboutToAppear(): void {
- if (this.restoredState) {
- try {
- const state = JSON.parse(this.restoredState);
- this.editContent = state.editText || '';
- console.info('恢复编辑内容成功');
- } catch (e) {
- console.error('恢复数据解析失败');
- }
- }
- }
- build() {
- Column() {
- TextArea({ text: this.editContent })
- .onChange((value: string) => {
- this.editContent = value;
- // 实时同步到 AppStorage,方便 onSaveState 读取
- AppStorage.set('editText', value);
- })
- }
- }
- }
复制代码 经验之谈:页面数据最好实时同步到 AppStorage,这样 onSaveState 被触发时能拿到最新的数据。别想着在 onSaveState 里再去读页面组件的数据,那个时机下页面可能已经销毁了,读不到。
和 appRecovery 的区别
这是这篇文章的重点。很多同学容易搞混。
appRecovery 是什么
简单回顾一下,appRecovery 是应用恢复模块,它做的事情是:
- 监听应用崩溃(JS Crash、C++ 异常、应用无响应)
- 崩溃后自动重启当前 Ability
- 可以选择保存 UI 状态,重启后恢复到之前的页面
核心 API 是 appRecovery.enableAppRecovery()。
两者的核心区别
||appRecovery|UIAbility 备份恢复|
|---|---|---|
|触发场景|应用在前台崩溃或ANR|应用在后台被系统杀掉|
|触发时机|崩溃发生时立即重启|下次冷启动时恢复|
|数据来源|崩溃前保存的UI状态|onSaveState保存的数据|
|页面栈恢复|自动恢复(包含UI状态)|仅恢复导航栈,不恢复页面内部状态|
|数据持久化|内存级别,重启设备丢失|文件级别,保留7天(重启设备丢失)|
|调用方式|appRecovery.enableAppRecovery()|context.setRestoreEnabled(true)|
|API支持|API 9+(不同能力分阶段)|API 14+|
怎么选
他俩不是二选一的关系,实际上是互补的。我的理解是:
- appRecovery 管的是“应用正在前台跑,突然崩了怎么办”——它的目标是让用户别看到崩溃白屏,直接悄悄重启回去
- UIAbility 备份恢复管的是“应用在后台被杀了,用户再打开时数据还在不在”——它的目标是让用户觉得应用从来没被杀过
我自己的项目里两个都开了。前台崩溃靠 appRecovery 兜底,后台被杀靠备份恢复保数据。不过要注意数据冲突的问题——两边都可能恢复数据,需要一个优先级策略。- // 同时开启两种恢复机制
- import { AbilityConstant, UIAbility, Want, appRecovery } from '@kit.AbilityKit';
- export default class EntryAbility extends UIAbility {
- onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- // 开启 appRecovery(崩溃后自动重启)
- appRecovery.enableAppRecovery(
- appRecovery.RestartFlag.ALWAYS_RESTART,
- appRecovery.SaveOccasionFlag.SAVE_OCCASION_UI_STATE
- );
- // 开启 UIAbility 备份恢复(后台被杀后恢复数据)
- this.context.setRestoreEnabled(true);
- // 读取恢复数据
- if (want && want.parameters && want.parameters['myAppState']) {
- // 来自 UIAbility 备份恢复的数据
- this.restoreFromBackup(want.parameters['myAppState']);
- }
- }
- private restoreFromBackup(data: Object): void {
- // 处理恢复的数据
- }
- onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
- // 保存状态...
- return AbilityConstant.OnSaveResult.ALL_AGREE;
- }
- }
复制代码 踩坑:两个机制都开的情况下,数据恢复的顺序要注意。appRecovery 是在崩溃后立即重启,这时候 UIAbility 备份恢复的数据还没写入(因为 onSaveState 可能都没来得及触发)。所以 appRecovery 恢复的是崩溃前的瞬时状态,而备份恢复恢复的是更早之前的持久化状态。如果两份数据同时存在,以 appRecovery 恢复的为准——因为它更新的。
反面教材
错误写法:往 wantParam 里塞了 200KB+ 的数据- // 错误写法——数据超限
- onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
- // 假设有个很大的日志字符串
- const hugeData = this.collectAllLogs(); // 可能几百 KB
- wantParam['logs'] = hugeData;
- // 还塞了很多其他东西
- wantParam['userData'] = JSON.stringify(this.getAllUserData()); // 又几百 KB
- return AbilityConstant.OnSaveResult.ALL_AGREE;
- }
复制代码 这个写法问题在于:wantParam 序列化后总大小不能超过 200KB。超了数据就丢了,而且没有任何报错。你完全不知道数据没保存成功。
正确写法:精简数据,控制体积- // 正确写法——只保存关键状态
- onSaveState(reason: AbilityConstant.StateType, wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
- // 只保存最关键的几个字段,别什么都往里塞
- wantParam['lastPage'] = AppStorage.get<string>('currentPage') ?? 'home';
- wantParam['draftId'] = AppStorage.get<string>('draftId') ?? '';
- wantParam['scrollPos'] = AppStorage.get<number>('scrollY') ?? 0;
- // 大日志、图片数据别放这里
- return AbilityConstant.OnSaveResult.ALL_AGREE;
- }
复制代码 经验之谈:如果你不确定数据大小是否超限,可以在调试阶段把 wantParam 序列化成 JSON 然后 JSON.stringify().length 打印出来看看。超过 200KB 就做个裁剪。我当时是在 onSaveState 里打了个日志,发现有一份数据序列化后 350KB,难怪恢复不了。
踩坑记录
坑 1:setRestoreEnabled 调用时机不对
有次我把 setRestoreEnabled(true) 写到了 onForeground 里,结果完全不生效。- // 错误写法——调用晚了
- onForeground(): void {
- this.context.setRestoreEnabled(true); // 必须在 onForeground 之前调
- }
复制代码 官方文档明确说了:“需要在应用初始化阶段调用(onForeground 前),比如 UIAbility 的 onCreate。”后来改到 onCreate 里就好了。
坑 2:页面栈恢复没生效
如前面说的,页面栈恢复依赖好几层条件:
- module.json5 里不能设置 removeMissionAfterTerminate: true
- 设备必须支持任务保留机制(PC/2in1 设备不支持)
- 用户不能手动清理了任务卡片
我当时在平板(2in1 设备)上测试,发现页面栈就是恢复不了。查了文档才知道 2in1 设备不支持任务保留,所以备份恢复的页面栈功能在这些设备上是废的。踩坑:2in1 设备上备份恢复的页面栈能力不可用。但数据恢复(want.parameters)还是正常的。所以别指望在平板上能恢复到之前的页面。
坑 3:Want 参数在冷启动时覆盖了正常传参
有一次发现,应用被杀后重新打开,本该从 Deep Link 传进来的参数被备份恢复的 Want 数据覆盖了。排查后发现:冷启动时 onCreate 的 want 确实是新的(来自 Deep Link),但系统会把备份数据合并到 want.parameters 里。如果你的备份数据和 Deep Link 参数用了相同的 key,就会互相覆盖。- // 注意:备份恢复的数据和正常启动参数共用 want.parameters
- onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- // 如果 Deep Link 传了 'source',而备份也存了 'source',就会冲突
- const source = want.parameters?.['source'] as string;
- // ...
- }
复制代码
总结
UIAbility 备份恢复是一个轻量级的后台数据保活方案,适合保存编辑中的表单、滚动位置、当前页面等临时状态,但不能替代持久化存储。与 appRecovery 结合使用可以覆盖前台崩溃和后台被杀两种场景。核心要点:启用时机要在 onCreate 而不是 onForeground,数据体积不能超过 200KB,注意 removeMissionAfterTerminate 冲突和 2in1 设备的限制。如果你的应用需要保证“用户就算切到后台被杀了,回来还能接着写”,这个机制就非常实用。 |