自动重启还不够:怎么把用户崩溃前的页面状态一起恢复【鸿蒙心迹】

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的App!
📌 关注本专栏《零基础学鸿蒙开发》,一起变强!
每一节内容我都会持续更新,配图+代码+解释全都有,欢迎点个关注,不走丢,我是小白酷爱学习,我们一起上路 🚀
全文目录:
前言
应用崩溃后能自动重启,这只是第一步。真正让用户觉得体验好的,是重启之后看到的页面和崩溃前几乎一致——搜索词还在、列表滚动位置还在、正在填写的表单还在。
HarmonyOS 提供的 appRecovery 能力,不仅支持崩溃后自动重启,还支持在崩溃前保存应用状态,并在重启时通过 Want 参数把状态重新传入 UIAbility。这篇文章就专门讨论这条完整的链路:状态怎么保存、什么时候保存、重启后怎么读回来,以及哪些状态值得保存、哪些应该重新请求数据。
一、光靠自动重启,用户体验还差一截
appRecovery.restartApp() 或者配置了自动重启之后,应用确实能在 JS Crash、CPP Crash、App Freeze 这类故障后重新拉起。但重启后进入的是初始状态,相当于用户第一次打开应用。
用户视角是:我只是手滑触发了一个 bug,结果我填了一半的内容没了、我翻到第三页的列表回到第一页了、我打开的详情页消失了。
appRecovery 的状态保存机制解决的正是这个问题。核心思路:
- 在故障发生时(或进入后台时),系统回调 UIAbility 的
onSaveState(); - 开发者在这里把关键状态写入 wantParam;
- 重启后,系统把这份 wantParam 通过 Want 传回
onCreate()或onNewWant(); - 应用读取参数,恢复页面状态。
这条链路完全由 @ohos.app.ability.appRecovery 模块支撑,API 9 起支持,适用于 Stage 模型。
二、把官方接口先梳理清楚
2.1 模块导入
import appRecovery from '@ohos.app.ability.appRecovery';
2.2 enableAppRecovery() — 开启恢复能力
这是整条链路的开关,建议在 AbilityStage 的 onCreate() 中调用。
appRecovery.enableAppRecovery(
appRecovery.RestartFlag.ALWAYS_RESTART,
appRecovery.SaveOccasionFlag.SAVE_WHEN_ERROR | appRecovery.SaveOccasionFlag.SAVE_WHEN_BACKGROUND,
appRecovery.SaveModeFlag.SAVE_WITH_FILE
);
三个参数分别控制:
| 参数 | 枚举 | 含义 |
|---|---|---|
| RestartFlag | ALWAYS_RESTART | 故障后总是重启 |
| RestartFlag | NO_RESTART | 故障后不重启 |
| SaveOccasionFlag | SAVE_WHEN_ERROR | 发生错误时保存状态 |
| SaveOccasionFlag | SAVE_WHEN_BACKGROUND | 进入后台时保存状态 |
| SaveModeFlag | SAVE_WITH_FILE | 状态持久化到文件 |
| SaveModeFlag | SAVE_WITH_SHARED_MEMORY | 状态保存到共享内存(仅限同次进程生命周期) |
SaveOccasionFlag 支持按位或组合。生产场景通常建议同时开启 SAVE_WHEN_ERROR 和 SAVE_WHEN_BACKGROUND,后者能保障在后台被杀前也留下一份状态快照。
SAVE_WITH_FILE 是持久化方式,即便进程被彻底杀死也可以在下次启动时读到;SAVE_WITH_SHARED_MEMORY 只适合进程未退出的场景,用于重启速度更快的情况。
2.3 onSaveState() — 状态写入时机
这是 UIAbility 的生命周期回调,当系统触发保存时被调用。
onSaveState(reason: appRecovery.StateType, wantParam: Record<string, Object>): boolean {
// 把需要恢复的状态写入 wantParam
wantParam['currentPage'] = 'DetailPage';
wantParam['articleId'] = 12345;
wantParam['scrollOffset'] = 320;
return true; // 返回 true 表示确认保存,false 表示放弃本次保存
}
reason 参数类型是 appRecovery.StateType,告知当前触发保存的原因:
appRecovery.StateType.APP_STATE_BACKGROUND:后台保存appRecovery.StateType.APP_STATE_FOREGROUND:前台异常触发保存
可以根据 reason 决定是否写入、写入哪些内容。
2.4 saveAppState() — 手动触发保存
除了系统自动触发外,也可以在业务逻辑里主动调用:
appRecovery.saveAppState();
典型场景:用户完成了一步关键操作(比如选择了城市、填写了某个重要字段),这时候主动保存一次,比等到崩溃再触发更保险。
2.5 重启后从 Want 参数读取状态
系统重启应用时,之前写入 wantParam 的内容会出现在 UIAbility 的 onCreate(want) 或 onNewWant(want) 的 want.parameters 里。
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const params = want.parameters;
if (params) {
const currentPage = params['currentPage'] as string;
const articleId = params['articleId'] as number;
const scrollOffset = params['scrollOffset'] as number;
// 根据这些参数决定恢复逻辑
}
}
判断是否是恢复启动,可以检查 launchParam.launchReason:
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
if (launchParam.launchReason === AbilityConstant.LaunchReason.APP_RECOVERY) {
// 这是一次恢复启动,可以读取 want.parameters 中的状态
}
三、搭一个最小示例
场景:应用有一个文章详情页,用户打开了某篇文章,滚动到了一定位置,突然发生崩溃。重启后,希望能回到同一篇文章的同一滚动位置。
3.1 在 MyAbilityStage 中启用恢复
// MyAbilityStage.ets
import AbilityStage from '@ohos.app.ability.AbilityStage';
import appRecovery from '@ohos.app.ability.appRecovery';
export default class MyAbilityStage extends AbilityStage {
onCreate(): void {
appRecovery.enableAppRecovery(
appRecovery.RestartFlag.ALWAYS_RESTART,
appRecovery.SaveOccasionFlag.SAVE_WHEN_ERROR |
appRecovery.SaveOccasionFlag.SAVE_WHEN_BACKGROUND,
appRecovery.SaveModeFlag.SAVE_WITH_FILE
);
}
}
记得在 module.json5 里把 srcEntry 指向这个 AbilityStage:
{
"module": {
"srcEntry": "./ets/myabilitystage/MyAbilityStage.ets"
}
}
3.2 在 EntryAbility 中保存和读取状态
// EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import appRecovery from '@ohos.app.ability.appRecovery';
import window from '@ohos.window';
// 用于在 Ability 和页面之间传递恢复参数
export let recoveryParams: Record<string, Object> = {};
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (launchParam.launchReason === AbilityConstant.LaunchReason.APP_RECOVERY) {
// 恢复启动,读取之前保存的状态
if (want.parameters) {
recoveryParams = want.parameters as Record<string, Object>;
}
}
}
onSaveState(reason: appRecovery.StateType, wantParam: Record<string, Object>): boolean {
// 从 AppStorage 或全局变量中读取当前页面状态
const articleId = AppStorage.get<number>('currentArticleId');
const scrollOffset = AppStorage.get<number>('currentScrollOffset');
const pageName = AppStorage.get<string>('currentPageName');
if (articleId !== undefined) {
wantParam['articleId'] = articleId;
}
if (scrollOffset !== undefined) {
wantParam['scrollOffset'] = scrollOffset;
}
if (pageName !== undefined) {
wantParam['pageName'] = pageName;
}
return true;
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index');
}
}
3.3 页面侧:更新状态 + 关键操作后手动保存
// pages/DetailPage.ets
import appRecovery from '@ohos.app.ability.appRecovery';
import { recoveryParams } from '../entryability/EntryAbility';
@Entry
@Component
struct DetailPage {
@State articleId: number = 0;
@State scrollOffset: number = 0;
private scroller: Scroller = new Scroller();
aboutToAppear(): void {
// 优先从恢复参数还原
if (recoveryParams['articleId']) {
this.articleId = recoveryParams['articleId'] as number;
}
if (recoveryParams['scrollOffset']) {
this.scrollOffset = recoveryParams['scrollOffset'] as number;
}
// 同步到 AppStorage,供 onSaveState 读取
AppStorage.setOrCreate('currentArticleId', this.articleId);
AppStorage.setOrCreate('currentPageName', 'DetailPage');
}
build() {
Scroll(this.scroller) {
Column() {
// 文章内容
Text(`文章 ID:${this.articleId}`)
.fontSize(16)
.padding(16)
}
}
.onScroll((xOffset: number, yOffset: number) => {
// 实时同步滚动位置到 AppStorage
const offset = this.scroller.currentOffset().yOffset;
AppStorage.setOrCreate('currentScrollOffset', offset);
})
.onAppear(() => {
// 页面出现后恢复滚动位置
if (this.scrollOffset > 0) {
this.scroller.scrollTo({ xOffset: 0, yOffset: this.scrollOffset });
}
})
}
}
页面打开时,通过 recoveryParams 判断是否有恢复数据;滚动过程中,实时把偏移量写入 AppStorage,这样 onSaveState 触发时就能读到最新值。
3.4 关键操作后主动保存一次
// 用户选择了某个重要选项后,主动触发一次状态保存
Button('确认城市')
.onClick(() => {
AppStorage.setOrCreate('selectedCity', '上海');
// 主动保存,不等故障触发
appRecovery.saveAppState();
})
四、几个关键点拆开看
4.1 onSaveState 里不要做重操作
这个回调是在故障或后台切换时触发的,执行时间有限。不要在里面做网络请求、大量 I/O 或复杂计算。只做一件事:把当前状态的关键标识快速写入 wantParam。
真正的数据不需要放进去,放的是"去哪里找数据的线索"——比如文章 ID、页面名称、选中的 Tab 索引、滚动偏移量。
4.2 wantParam 的值类型有限制
wantParam 是 Record<string, Object> 类型,但实际存入的值只支持基础类型:string、number、boolean,以及这些类型组成的数组。不支持直接存放自定义对象或复杂嵌套结构。如果需要保存复杂状态,建议先 JSON.stringify() 序列化为字符串,读取时再 JSON.parse() 还原。
// 保存时
wantParam['filterConfig'] = JSON.stringify({ category: 'tech', sort: 'latest' });
// 恢复时
const raw = recoveryParams['filterConfig'] as string;
const filterConfig = JSON.parse(raw);
4.3 用 launchReason 区分恢复启动和正常启动
不判断 launchReason 直接读 want.parameters 是比较常见的失误。正常冷启动时 want.parameters 可能也有内容(来自其他拉起来源),混用会造成逻辑错误。
// 正确做法:先判断是否是恢复启动
if (launchParam.launchReason === AbilityConstant.LaunchReason.APP_RECOVERY) {
// 才读恢复参数
}
4.4 SAVE_WITH_SHARED_MEMORY 不适合真正的崩溃场景
共享内存方式在同一进程周期内有效,进程被彻底终止后数据会丢失。崩溃(JS Crash / CPP Crash)通常意味着进程已经退出,这种情况下只有 SAVE_WITH_FILE 能保住数据。选错模式会导致恢复时拿不到任何参数。
4.5 AbilityStage 必须正确配置
enableAppRecovery() 写在自定义 AbilityStage 里,但如果 module.json5 中没有正确设置 srcEntry,系统用的是默认 AbilityStage,自定义逻辑不会执行。这个问题在开发阶段比较隐蔽,因为应用照常运行,只是恢复能力静默失效了。
五、什么状态值得保存,什么状态应该重新加载
这是实际开发中最需要判断的部分。
适合写入 wantParam 的状态:
- 当前停留的页面/路由名称
- 页面的关键业务标识(文章 ID、商品 ID、订单号等)
- UI 层的临时状态(滚动偏移、选中的 Tab 索引、展开/收起状态)
- 用户正在编辑但尚未提交的轻量级文本(搜索词、表单关键字段)
- 筛选条件、排序方式
这类状态的特点是:体积小、无需网络、代表的是"用户在哪里、在做什么"。
不适合写入 wantParam、应该重新从数据库或服务器加载的状态:
- 列表数据本身(可能已经过期,重新请求更可靠)
- 图片、媒体资源(体积大,且有独立缓存机制)
- 完整的用户信息(应该从本地存储或服务器刷新)
- 敏感数据(token、密码等不应该出现在 Want 参数里)
- 超过合理体积限制的 JSON 序列化数据
核心判断标准:这份数据是"导航线索"还是"业务数据本身"?只保存线索,业务数据重新加载。
六、容易踩坑的地方
坑一:onSaveState 不返回 true
返回 false 或者没有返回值,系统会认为本次保存被放弃,wantParam 里的内容不会被持久化。一定要显式返回 true。
坑二:AppStorage 和 wantParam 没有同步
在 onSaveState 里从 AppStorage 读状态,但页面侧更新状态时没有同步写入 AppStorage,导致保存的是旧值。建议在每次关键状态变化时都同步更新 AppStorage,而不是等到 onSaveState 触发时再去"采集"。
坑三:多 UIAbility 场景
onSaveState 是 UIAbility 级别的回调,每个 UIAbility 分别保存自己的状态。如果应用有多个 UIAbility,需要在各自的 onSaveState 里分别处理,不能只配置主 Ability。
坑四:恢复参数在页面已经渲染完才去读
在 onWindowStageCreate 里加载页面内容之前,recoveryParams 已经可以赋值,页面 aboutToAppear 时就能读到。但如果通过异步方式延迟传参,可能出现页面先用默认值渲染、恢复参数到达后再刷新的闪烁问题。建议同步处理好参数再渲染。
七、排查顺序
如果配置了 appRecovery 但重启后状态没有恢复,按下面顺序检查:
- 先确认 API Level:
appRecovery从 API 9 开始支持,确认工程的compileSdkVersion和targetSdkVersion是否满足; - 确认 AbilityStage 配置:
module.json5里的srcEntry是否指向了调用enableAppRecovery()的自定义 AbilityStage; - 确认故障类型:
RestartFlag是否覆盖了当前发生的故障类型(JS Crash / CPP Crash / App Freeze); - 确认 SaveModeFlag:是否使用了
SAVE_WITH_FILE,尤其是在进程退出类故障场景; - 确认
onSaveState返回值:是否显式返回了true; - 确认 wantParam 写入时机:触发保存时 AppStorage 中是否已有最新值;
- 确认
launchReason判断:恢复启动时是否正确判断了APP_RECOVERY并读取了参数; - 检查 wantParam 值类型:是否有写入了不支持的复杂对象类型。
开发经验总结
enableAppRecovery()放在 AbilityStage 的onCreate()中,module.json5的srcEntry要同步配置,漏掉任何一步恢复能力都不会生效。onSaveState只负责写"恢复线索",不适合写大体积数据;数据本体在重启后重新加载。SAVE_WITH_FILE和SAVE_WITH_SHARED_MEMORY针对不同场景,崩溃场景务必用前者。- 用
launchParam.launchReason === AbilityConstant.LaunchReason.APP_RECOVERY明确区分恢复启动,不要在正常启动路径上误触发恢复逻辑。 saveAppState()可以在关键操作节点主动调用,作为故障触发保存的补充,提升状态快照的时效性。
如果你现在的应用已经配置了自动重启,可以进一步观察:用户最常停留的页面是哪些?这些页面上哪些状态丢失后用户需要重新操作最多步骤?从这里入手决定写入 wantParam 的内容,比盲目保存所有状态要实用得多。
❤️ 如果本文帮到了你…
- 请点个赞,让我知道你还在坚持阅读技术长文!
- 请收藏本文,因为你以后一定还会用上!
- 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
更多推荐



所有评论(0)