鸿蒙应用与页面生命周期

本文梳理 HarmonyOS NEXT Stage 模型下的应用与页面生命周期。覆盖 UIAbility 的 onCreate、onWindowStageCreate、onForeground、onBackground、onWindowStageDestroy、onDestroy 六个回调,AbilityStage 模块级初始化,三种 launchType 的差异,以及页面与组件生命周期对照。

引言

HarmonyOS NEXT 全面采用 Stage 模型,应用的生命周期不再是单一 Ability 的一串回调,而是「进程到 AbilityStage、AbilityStage 到 UIAbility、UIAbility 到 WindowStage、WindowStage 到页面、页面到组件」五层嵌套。每一层都有各自的创建与销毁时机,层与层之间的先后顺序直接决定了「在哪个回调里做什么事」是安全的。

实践中出问题最多的地方,恰恰是这些顺序假设:在 onCreate 里做耗时初始化导致冷启动白屏;onWindowStageCreate 里忘了 loadContent 导致页面永远不显示;把 onPageShow 和 aboutToAppear 的触发次数当成一样;onBackPress 没有显式 return false 导致返回键失效。本文从 UIAbility 的完整可运行代码出发,逐层拆解回调职责、launchType 对生命周期的影响、前后台切换的资源处理,以及进程回收后的状态恢复。

前置阅读:鸿蒙页面路由与 Navigation 组件 。

Stage 模型生命周期全景

Stage 模型把一个应用拆成「一个进程 + 若干模块(Module)+ 若干 Ability」。启动链路是:系统先创建进程并拉起模块的 AbilityStage,再由 AbilityStage 创建具体的 UIAbility 实例,UIAbility 创建窗口舞台(WindowStage),窗口舞台加载页面(loadContent),页面渲染出自定义组件。

这条链路上的关键约束是层层递进、不可跳级:

  • AbilityStage.onCreate 一定早于该模块内任何 UIAbility.onCreate。
  • UIAbility.onWindowStageCreate 一定晚于 UIAbility.onCreate,且 windowStage 参数只在这一次回调里有效。
  • 页面组件的 aboutToAppear 一定晚于 onWindowStageCreate 中的 loadContent 调用。
  • 销毁顺序与创建顺序相反:组件 aboutToDisappear → 页面 onPageHide → onWindowStageDestroy → onDestroy。

UIAbility 的完整生命周期代码

下面是一个覆盖全部回调的 EntryAbility,可以直接作为工程模板使用。注意每个回调里做的事情都刻意保持轻量,这一点后面会展开讲原因。

// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

const DOMAIN = 0x0000;
const TAG = 'EntryAbility';

export default class EntryAbility extends UIAbility {
  private mainWindow: window.Window | undefined = undefined;

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN, TAG, `onCreate reason=${launchParam.launchReason}`);
    // 只做轻量同步初始化,耗时任务交给 onWindowStageCreate 之后
    AppStorage.setOrCreate('launchReason', launchParam.launchReason);
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err: BusinessError) => {
      if (err.code) {
        hilog.error(DOMAIN, TAG, `loadContent failed: ${err.code}`);
        return;
      }
      this.mainWindow = windowStage.getMainWindowSync();
      this.mainWindow.setWindowLayoutFullScreen(true);
    });
  }

  onForeground(): void {
    hilog.info(DOMAIN, TAG, 'onForeground');
  }

  onBackground(): void {
    hilog.info(DOMAIN, TAG, 'onBackground');
  }

  onWindowStageDestroy(): void {
    this.mainWindow = undefined;
  }

  onDestroy(): void {
    hilog.info(DOMAIN, TAG, 'onDestroy');
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN, TAG, `onNewWant uri=${want.uri}`);
  }

  onSaveState(reason: AbilityConstant.StateType,
    wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
    wantParam['scrollOffset'] = AppStorage.get<number>('scrollOffset') ?? 0;
    return AbilityConstant.OnSaveResult.ALL_AGREE;
  }
}

onWindowStageDestroy 里把 mainWindow 置空不是可选项:窗口销毁后旧引用继续调用 setWindowLayoutFullScreen 之类的接口会抛 1300001(无权限)或直接崩溃。

逐个回调的职责边界

onCreate

onCreate(want, launchParam) 是实例创建后拿到的第一次机会,want 携带拉起参数(uri、parameters、action),launchParam 携带启动原因 launchReason。

这里能做的事:读取 want 参数、注册全局单例、写入 AppStorage 初始值。
这里不能做的事:同步 I/O、数据库建表、网络请求、await 长耗时任务。这些操作会直接阻塞冷启动的第一帧,用户看到的是白屏。

如果确实有耗时初始化,正确做法是放到 onWindowStageCreate 的 loadContent 回调之后,或者用 taskpool 异步执行,页面先用骨架屏占位。并发任务的写法可以参考 鸿蒙并发与 TaskPool 。

onWindowStageCreate

onWindowStageCreate(windowStage) 是唯一能拿到 WindowStage 的入口,loadContent 必须在这里调用,否则窗口是空的,页面永远不会出现。这是新手最容易漏掉的一步。

onWindowStageCreate(windowStage: window.WindowStage): void {
  // 第二个参数是 LocalStorage,可用于向页面注入初始状态
  const storage = new LocalStorage({ themeMode: 'light' });
  windowStage.loadContent('pages/Index', storage, (err: BusinessError) => {
    if (err.code) { hilog.error(DOMAIN, TAG, `loadContent failed: ${err.code}`); }
  });
}

loadContent 的三个重载分别是 (path, callback)、(path, storage, callback) 和 (path, storage): Promise<void>。传入 LocalStorage 后,页面里用 @LocalStorageProp('themeMode') 就能读到,这是「Ability 层向 UI 层传初始值」的官方通道。

onForeground 与 onBackground

onForeground 在 Ability 切到前台、UI 变得可见之前触发;onBackground 在切到后台、UI 已不可见之后触发。两者的配对关系并不严格——系统可能在没走 onBackground 的情况下直接销毁进程,所以不要假设「onBackground 一定会被调用」。

适合放在 onForeground 的:恢复轮询、重新订阅传感器、刷新需要实时性的数据。
适合放在 onBackground 的:暂停定时器、断开长连接、释放大块缓存(尤其是图片解码后的 PixelMap)。

注意这两个回调的粒度是 Ability 级,不是页面级。页面级的可见性变化要走 onPageShow / onPageHide。

onWindowStageDestroy 与 onDestroy

onWindowStageDestroy 在窗口舞台销毁时触发,此时 window 对象即将失效,是解绑窗口事件、置空引用的最后时机。onDestroy 在 Ability 实例销毁时触发,用于释放全局资源、注销监听器、关闭数据库连接。

API 12 新增了 onWindowStageWillDestroy,它在舞台销毁之前触发,此时窗口仍然可用,适合做「保存窗口尺寸」这类需要读窗口状态的操作。

AbilityStage 与模块级初始化

AbilityStage 是模块级的入口,对应 module.json5 中模块级的 srcEntry 字段。它只在该模块的进程首次启动时创建一次,早于模块内任何 UIAbility。

// entry/src/main/ets/entryability/MyAbilityStage.ets
import { AbilityStage, Want } from '@kit.AbilityKit';

export default class MyAbilityStage extends AbilityStage {
  onCreate(): void {
    // 模块级初始化只跑一次,适合注册全局异常处理、初始化日志、预热配置
    AppStorage.setOrCreate('appStartTime', Date.now());
  }

  onAcceptWant(want: Want): string {
    // 仅当目标 Ability 的 launchType 为 specified 时生效
    return `doc_${want.parameters?.['docId'] ?? 'default'}`;
  }

  onMemoryLevel(level: number): void {
    // 系统内存告警回调,level 越低内存越紧张,按档释放缓存
  }
}
{
  "module": {
    "name": "entry",
    "type": "entry",
    "srcEntry": "./ets/entryability/MyAbilityStage.ets"
  }
}

AbilityStage 与 UIAbility 的职责划分很简单:跟具体 Ability 无关、整个模块只需要做一次的事放 AbilityStage;跟某次拉起参数相关的事放 UIAbility。 日志框架初始化、崩溃捕获、全局配置读取都属于前者。

onNewWant 与 launchType

launchType 在 module.json5 的 abilities 数组中配置,决定「同一个 Ability 被重复拉起时怎么办」,并直接影响 onNewWant 是否触发。

{
  "module": {
    "name": "entry",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "launchType": "singleton",
        "exported": true,
        "skills": [{ "entities": ["entity.system.home"], "actions": ["action.system.home"] }]
      }
    ]
  }
}
launchType实例策略onNewWant典型场景
singleton全应用仅一个实例,重复拉起复用触发绝大多数应用的入口 Ability
multiton每次拉起都新建实例不触发需要并行多实例,如多账号同时在线
specified由 AbilityStage.onAcceptWant 的返回值决定复用还是新建视返回值按业务 ID 去重,如多文档编辑

onNewWant(want, launchParam) 只在 singleton 实例被再次拉起 时触发,它是拿到新 want 参数的唯一入口:

onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  const target = want.parameters?.['targetPage'] as string;
  // 通过 AppStorage 通知页面跳转,不要在 Ability 里直接操作 UI
  if (target !== undefined) { AppStorage.setOrCreate('pendingRoute', target); }
}

这里有个硬约束:onNewWant 里拿不到 WindowStage,也没有 UIContext,因此不能直接做页面跳转。标准做法是把目标路由写进 AppStorage,由页面在 onPageShow 里读取并消费。

WindowStage 与窗口管理

WindowStage 是 UIAbility 与窗口系统的桥梁,除 loadContent 外还提供主窗口获取与生命周期监听:

onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.loadContent('pages/Index', (err: BusinessError) => {
    if (err.code) { return; }
    const mainWindow = windowStage.getMainWindowSync();
    // 沉浸式布局:内容延伸到状态栏区域
    mainWindow.setWindowLayoutFullScreen(true);
    mainWindow.setWindowSystemBarProperties({
      statusBarContentColor: '#FFFFFF'
    });
  });
  // 监听舞台生命周期,用于多窗口与分屏场景
  windowStage.on('windowStageEvent', (data: window.WindowStageEventType) => {
    hilog.info(DOMAIN, TAG, `stage event=${data}`);
  });
}

getMainWindowSync() 是同步接口,只能在窗口创建完成后调用;在 onCreate 里调会返回 undefined 或抛异常。多窗口、分屏、悬浮窗场景下,一个 WindowStage 可能持有多个 Window,此时要用 getMainWindow() 的异步版本逐个获取。

页面与组件生命周期

页面和组件是两层不同的概念:页面特指被 @Entry 装饰的 struct,享有 onPageShow / onPageHide / onBackPress 三个页面级回调;自定义组件指任意 @Component 装饰的 struct,只有 aboutToAppear / onDidBuild / aboutToDisappear。

Entry 页面的三个回调

// pages/Index.ets
@Entry
@Component
struct Index {
  @State count: number = 0;

  onPageShow(): void {
    // 页面每次可见都触发,包括从其他页面返回
    console.info('onPageShow');
    const pending = AppStorage.get<string>('pendingRoute');
    if (pending !== undefined) {
      console.info(`consume pending route: ${pending}`);
      AppStorage.setOrCreate('pendingRoute', undefined);
    }
  }

  onPageHide(): void {
    // 页面不可见,但实例仍然存在
    console.info('onPageHide');
  }

  onBackPress(): boolean {
    // 返回 true 表示已自行处理,系统不再执行默认返回逻辑
    if (this.count > 0) {
      this.count = 0;
      return true;
    }
    return false;
  }

  build() {
    Column({ space: 8 }) {
      Text(`count = ${this.count}`).fontSize(20)
      Button('加一').onClick(() => { this.count++; })
    }
    .width('100%')
    .padding(16)
  }
}

onPageShow 与 aboutToAppear 的区别是本篇最需要记牢的一点:aboutToAppear 只在页面实例首次创建时执行一次,而 onPageShow 在每次页面变为可见时都执行。用 router 的 Single 模式复用页面时,aboutToAppear 不会重跑,只有 onPageShow 会;Navigation 栈内的页面遵循同一规律,只是回调换成了 onShown 与 onWillShow。

自定义组件的三个回调

@Component
struct ChildPanel {
  @Prop title: string;
  private timerId: number = -1;

  aboutToAppear(): void {
    // build 之前调用,适合准备数据;禁止放耗时同步操作
    this.timerId = setInterval(() => {
      console.info('tick');
    }, 1000);
  }

  onDidBuild(): void {
    // API 12 新增,build 执行完成后调用,适合做埋点与布局完成后的测量
    console.info('ChildPanel built');
  }

  aboutToDisappear(): void {
    // 组件销毁前调用,定时器、监听、订阅必须在这里清理
    clearInterval(this.timerId);
  }

  build() {
    Text(this.title).fontSize(16)
  }
}

onDidBuild 是 API 12 才有的回调,它解决了一个老问题:想在「布局完成」之后做一次上报或测量,过去只能靠 setTimeout(0) 硬凑时机,现在有明确的位置。注意它不会在状态变化导致的重新渲染时再次触发,只在组件首次 build 完成后触发一次。

onBackPress 的返回值语义

onBackPress(): boolean 的返回值是整个返回链路里语义最容易被忽略的地方:

  • 返回 true:表示「我已经处理了这次返回」,系统不再执行默认行为(弹栈或退出应用)。
  • 返回 false 或没有返回值(void):走默认行为。

写 onBackPress() { this.count = 0; } 这种没有返回值的实现,等于什么都没拦截,返回键依然会正常退出——很多人以为「写了这个函数就拦住了」,结果线上收到「返回键没反应」的反馈,实际是自己把返回值漏了。在 Navigation 页面里没有 onBackPress,要用 NavDestination.onBackPressed,语义相同。

应用前后台切换与资源释放

前后台切换是本篇最需要权衡的一节。切到后台时释放资源能显著降低内存占用、减少被系统回收的概率,但释放错了会导致切回来时状态丢失。推荐的资源分级策略如下:

资源类型切后台处理切回前台处理
定时器、轮询暂停,记录已运行时长恢复,必要时立即补一次
长连接、WebSocket保留(断开会丢消息),但降低心跳频率恢复正常心跳
解码后的 PixelMap、Bitmap立即释放,只保留原始文件路径按需重新解码
数据库连接保留,关闭重开代价高于收益无需处理
传感器订阅取消订阅重新订阅
位置、相机等敏感权限会话必须释放按需重新申请

判断依据只有一条:这份资源能不能在不丢用户可见状态的前提下重建。 能重建的就释放,不能重建的就保留。

进程回收与状态保存

应用在后台时,系统会按内存压力回收进程。被回收前,如果 Ability 实现了 onSaveState,系统会给一次保存状态的机会:

onSaveState(reason: AbilityConstant.StateType,
  wantParam: Record<string, Object>): AbilityConstant.OnSaveResult {
  wantParam['scrollOffset'] = AppStorage.get<number>('scrollOffset') ?? 0;
  wantParam['draft'] = AppStorage.get<string>('draft') ?? '';
  return AbilityConstant.OnSaveResult.ALL_AGREE;
}

返回值 OnSaveResult 有三个取值:ALL_AGREE(同意保存并退出)、ALL_REFUSE(拒绝保存)、CONTINUATION_ONLY(仅迁移场景保存)。返回 ALL_REFUSE 会阻止本次保存,一般不要用。

恢复走的是 onCreate,不是独立回调:

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  if (launchParam.launchReason === AbilityConstant.LaunchReason.APP_RECOVERY) {
    const offset = want.parameters?.['scrollOffset'] as number ?? 0;
    AppStorage.setOrCreate('scrollOffset', offset);
  }
}

注意这里有个常见的概念混淆:Stage 模型的 UIAbility 并没有一个叫 onRestoreState 的独立回调,恢复数据是通过重新拉起时 onCreate 的 want.parameters 传进来的,判断依据是 launchParam.launchReason。

AppStorage 与 PersistentStorage

onSaveState 只覆盖「被系统回收」这一种场景,应用主动重启、用户杀进程都走不到。要覆盖这些场景,用 PersistentStorage 把关键状态落盘:

// 声明即持久化:首次启动写入默认值,之后自动读取落盘值
PersistentStorage.persistProp('themeMode', 'light');
PersistentStorage.persistProps([
  { key: 'draft', defaultValue: '' },
  { key: 'scrollOffset', defaultValue: 0 }
]);

// 之后对 AppStorage 的读写会自动同步到持久化存储
AppStorage.setOrCreate('themeMode', 'dark');
const mode = AppStorage.get<string>('themeMode');

persistProps 是 API 12 新增的批量版本,比逐个调用 persistProp 少一次初始化开销。要记住两条边界:PersistentStorage 只支持基本类型与可序列化的简单对象,不能存 PixelMap、UIContext 这类宿主对象;它的读写是同步的,不要在 UI 高频路径上反复调用。

UIAbility 生命周期回调时序表

回调触发时机典型用途
onCreate实例创建后,早于窗口创建读取 want 参数、轻量全局状态初始化
onWindowStageCreate窗口舞台创建后loadContent 加载首页、设置窗口属性
onForeground切到前台,UI 可见之前恢复轮询、重新订阅传感器、刷新实时数据
onBackground切到后台,UI 不可见之后暂停定时器、释放可重建的大块缓存
onWindowStageWillDestroyAPI 12 新增,舞台销毁前读取并保存窗口尺寸等状态
onWindowStageDestroy窗口舞台销毁时解绑窗口事件、置空 window 引用
onDestroy实例销毁时释放全局资源、注销监听、关闭连接
onNewWantsingleton 实例被再次拉起处理新 want 参数,转交页面消费
onSaveState系统回收或迁移前保存需要恢复的状态并返回 OnSaveResult

页面与组件生命周期对照表

回调作用范围触发次数与 Navigation 的对应
onPageShow@Entry 页面每次页面可见NavDestination.onShown
onPageHide@Entry 页面每次页面不可见NavDestination.onHidden
onBackPress@Entry 页面用户按下返回键NavDestination.onBackPressed
aboutToAppear任意 @Component实例创建后仅一次无直接对应
onDidBuild任意 @Component首次 build 完成后仅一次无直接对应
aboutToDisappear任意 @Component实例销毁前仅一次NavDestination.onWillDisappear

权衡取舍

后台保活是很多团队的第一反应,但在 HarmonyOS NEXT 上这条路很窄。系统对后台进程有明确的管控策略:应用退到后台后,长时任务需要申请对应的 backgroundTaskManager 长时任务类型(如 DATA_TRANSFER、LOCATION、AUDIO_PLAYBACK),未申请的后台运行会被系统冻结甚至回收。因此正确的思路不是「怎么让进程活着」,而是「怎么让被回收后能无缝恢复」:

  1. 状态分层:把「必须恢复的」写进 PersistentStorage,把「可以重建的」放在内存里随时重建。
  2. 幂等恢复:恢复逻辑要能容忍「数据存在但页面已重建」「页面还在但数据被清」两种组合。
  3. 少用全局单例:Ability 销毁后单例里的引用会悬空,改用 AppStorage 或随组件生命周期管理的对象。

代价是恢复逻辑本身有复杂度:要区分首次启动、正常恢复、异常恢复三条路径,且每条路径都要能走通。相比申请长时任务被拒、审核被拒,这点复杂度是划算的。

常见坑清单

  1. 在 onCreate 里做耗时操作阻塞启动。 数据库初始化、同步网络请求、大文件读取都会让首帧推迟。移到 loadContent 回调之后,或交给 taskpool 异步执行。
  2. onWindowStageCreate 里未调用 loadContent。 窗口创建成功但内容为空,表现为白屏且没有任何报错,排查时优先确认这一点。
  3. 把 onPageShow 和 aboutToAppear 的触发次数当成一样。 aboutToAppear 只在实例创建时一次,onPageShow 每次可见都触发。页面复用场景下数据刷新必须写在 onPageShow。
  4. onBackPress 不返回导致返回失效。 拦截返回必须显式 return true,只写逻辑不写返回值等于没拦截。
  5. 多实例 launchType 状态串扰。 用 multiton 时每个实例有独立的 AppStorage 之外的全局单例会互相覆盖;全局状态要么放 AppStorage,要么按实例 ID 加命名空间。
  6. 在 onNewWant 里直接跳页面。 此时拿不到 WindowStage 与 UIContext,必须把意图写进 AppStorage 由页面消费。
  7. 误以为存在 onRestoreState 独立回调。 Stage 模型的 UIAbility 只有 onSaveState,恢复走重新拉起时 onCreate 的 want.parameters。
  8. aboutToDisappear 里没清理定时器与监听。 组件销毁后定时器仍在跑,回调里访问已销毁的 @State 会抛异常,这是页面级内存泄漏的头号来源。
  9. 依赖 onBackground 一定会被调用。 系统可能直接回收进程而不触发 onBackground,关键状态要在产生时就落盘,不要等这个回调。

小结

Stage 模型的生命周期是一条五层嵌套的链路:AbilityStage 负责模块级一次性初始化,UIAbility 负责实例与窗口,WindowStage 负责窗口与内容加载,页面负责可见性与返回拦截,组件负责资源申请与释放。理解它的关键不是背下九个回调的名字,而是记住三条判断原则:能在创建后重建的放在前面做,需要窗口才能做的放在 onWindowStageCreate 之后,需要感知可见性的放在 onPageShow 里。

再补一句工程上的建议:把「生命周期回调里做了什么」当成一次代码评审的固定检查项,绝大多数启动白屏、返回失效、内存泄漏问题都能在这条链路上提前发现。想继续深入启动耗时与卡顿定位,可以看 鸿蒙性能调优与调试 ;如果初始化里有耗时任务需要异步化,并发与 TaskPool 那一篇给了完整的落地方案。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「鸿蒙开发」更多文章

  1. 鸿蒙 ohpm 包管理与 Hypium 测试框架
  2. ArkUI 动画体系与手势交互
  3. 鸿蒙应用安全:权限模型与 HUKS 密钥管理