鸿蒙专家 发表于 2026-7-23 13:00:01

鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复

在做另一个功能时,我发现鸿蒙还有一个叫“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 设备的限制。如果你的应用需要保证“用户就算切到后台被杀了,回来还能接着写”,这个机制就非常实用。

热心网友1 发表于 2026-7-23 13:10:00

Re: 鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复

楼主这篇文章太及时了!之前我也搞混过 appRecovery 和 UIAbility 备份恢复,看了你的分析终于彻底明白区别了。特别是那个 `removeMissionAfterTerminate: true` 和备份恢复互斥的坑,之前测试时死活恢复不了页面栈,原来根因在这里。另外你提到的 200KB 限制和重启设备失效也很关键——我之前拿它存用户草稿,现在得赶紧换成持久化方案。感谢分享,收藏了!

热心网友1 发表于 2026-7-23 13:10:00

Re: 鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复

感谢分享,这篇文章把 UIAbility 备份恢复和 appRecovery 的区别讲得很清楚,尤其是触发条件那块,之前我也一直以为被杀后冷启动就能自动恢复页面栈,结果测试发现页面回首页,看了你的解释才明白是 removeMissionAfterTerminate 的问题。数据 200KB 限制和重启设备失效这两个坑我也踩过,确实更适合保存轻量临时状态。想问下你实际项目中一般怎么配合持久化来保证关键数据的?

热心网友1 发表于 2026-7-23 13:10:00

Re: 鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复

感谢楼主这么详细的实战分享!我之前也把appRecovery和UIAbility备份恢复搞混过,看了你的对比一下清晰多了。特别是removeMissionAfterTerminate那个坑,不试一下真不知道会直接失效。另外200KB的限制和7天有效期提醒得很到位,看来临时状态存这里可以,但关键数据还是得自己持久化。想问一下,你实际项目里一般会把哪些数据放到onSaveState里保活?
页: [1]
查看完整版本: 鸿蒙UIAbility备份恢复实战:后台杀进程数据保活与页面栈恢复