在这里插入图片描述

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的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
);

三个参数分别控制:

参数枚举含义
RestartFlagALWAYS_RESTART故障后总是重启
RestartFlagNO_RESTART故障后不重启
SaveOccasionFlagSAVE_WHEN_ERROR发生错误时保存状态
SaveOccasionFlagSAVE_WHEN_BACKGROUND进入后台时保存状态
SaveModeFlagSAVE_WITH_FILE状态持久化到文件
SaveModeFlagSAVE_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 但重启后状态没有恢复,按下面顺序检查:

  1. 先确认 API Level:appRecovery 从 API 9 开始支持,确认工程的 compileSdkVersion 和 targetSdkVersion 是否满足;
  2. 确认 AbilityStage 配置:module.json5 里的 srcEntry 是否指向了调用 enableAppRecovery() 的自定义 AbilityStage;
  3. 确认故障类型:RestartFlag 是否覆盖了当前发生的故障类型(JS Crash / CPP Crash / App Freeze);
  4. 确认 SaveModeFlag:是否使用了 SAVE_WITH_FILE,尤其是在进程退出类故障场景;
  5. 确认 onSaveState 返回值:是否显式返回了 true;
  6. 确认 wantParam 写入时机:触发保存时 AppStorage 中是否已有最新值;
  7. 确认 launchReason 判断:恢复启动时是否正确判断了 APP_RECOVERY 并读取了参数;
  8. 检查 wantParam 值类型:是否有写入了不支持的复杂对象类型。

开发经验总结

  • enableAppRecovery() 放在 AbilityStage 的 onCreate() 中,module.json5 的 srcEntry 要同步配置,漏掉任何一步恢复能力都不会生效。
  • onSaveState 只负责写"恢复线索",不适合写大体积数据;数据本体在重启后重新加载。
  • SAVE_WITH_FILE 和 SAVE_WITH_SHARED_MEMORY 针对不同场景,崩溃场景务必用前者。
  • 用 launchParam.launchReason === AbilityConstant.LaunchReason.APP_RECOVERY 明确区分恢复启动,不要在正常启动路径上误触发恢复逻辑。
  • saveAppState() 可以在关键操作节点主动调用,作为故障触发保存的补充,提升状态快照的时效性。

如果你现在的应用已经配置了自动重启,可以进一步观察:用户最常停留的页面是哪些?这些页面上哪些状态丢失后用户需要重新操作最多步骤?从这里入手决定写入 wantParam 的内容,比盲目保存所有状态要实用得多。

❤️ 如果本文帮到了你…

  • 请点个赞,让我知道你还在坚持阅读技术长文!
  • 请收藏本文,因为你以后一定还会用上!
  • 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐