引言
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 不可见之后 | 暂停定时器、释放可重建的大块缓存 |
| onWindowStageWillDestroy | API 12 新增,舞台销毁前 | 读取并保存窗口尺寸等状态 |
| onWindowStageDestroy | 窗口舞台销毁时 | 解绑窗口事件、置空 window 引用 |
| onDestroy | 实例销毁时 | 释放全局资源、注销监听、关闭连接 |
| onNewWant | singleton 实例被再次拉起 | 处理新 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),未申请的后台运行会被系统冻结甚至回收。因此正确的思路不是「怎么让进程活着」,而是「怎么让被回收后能无缝恢复」:
- 状态分层:把「必须恢复的」写进
PersistentStorage,把「可以重建的」放在内存里随时重建。 - 幂等恢复:恢复逻辑要能容忍「数据存在但页面已重建」「页面还在但数据被清」两种组合。
- 少用全局单例:Ability 销毁后单例里的引用会悬空,改用
AppStorage或随组件生命周期管理的对象。
代价是恢复逻辑本身有复杂度:要区分首次启动、正常恢复、异常恢复三条路径,且每条路径都要能走通。相比申请长时任务被拒、审核被拒,这点复杂度是划算的。
常见坑清单
- 在 onCreate 里做耗时操作阻塞启动。 数据库初始化、同步网络请求、大文件读取都会让首帧推迟。移到
loadContent回调之后,或交给taskpool异步执行。 - onWindowStageCreate 里未调用 loadContent。 窗口创建成功但内容为空,表现为白屏且没有任何报错,排查时优先确认这一点。
- 把 onPageShow 和 aboutToAppear 的触发次数当成一样。
aboutToAppear只在实例创建时一次,onPageShow每次可见都触发。页面复用场景下数据刷新必须写在onPageShow。 - onBackPress 不返回导致返回失效。 拦截返回必须显式
return true,只写逻辑不写返回值等于没拦截。 - 多实例 launchType 状态串扰。 用
multiton时每个实例有独立的AppStorage之外的全局单例会互相覆盖;全局状态要么放AppStorage,要么按实例 ID 加命名空间。 - 在 onNewWant 里直接跳页面。 此时拿不到
WindowStage与UIContext,必须把意图写进AppStorage由页面消费。 - 误以为存在 onRestoreState 独立回调。 Stage 模型的 UIAbility 只有
onSaveState,恢复走重新拉起时onCreate的want.parameters。 - aboutToDisappear 里没清理定时器与监听。 组件销毁后定时器仍在跑,回调里访问已销毁的
@State会抛异常,这是页面级内存泄漏的头号来源。 - 依赖 onBackground 一定会被调用。 系统可能直接回收进程而不触发
onBackground,关键状态要在产生时就落盘,不要等这个回调。
小结
Stage 模型的生命周期是一条五层嵌套的链路:AbilityStage 负责模块级一次性初始化,UIAbility 负责实例与窗口,WindowStage 负责窗口与内容加载,页面负责可见性与返回拦截,组件负责资源申请与释放。理解它的关键不是背下九个回调的名字,而是记住三条判断原则:能在创建后重建的放在前面做,需要窗口才能做的放在 onWindowStageCreate 之后,需要感知可见性的放在 onPageShow 里。
再补一句工程上的建议:把「生命周期回调里做了什么」当成一次代码评审的固定检查项,绝大多数启动白屏、返回失效、内存泄漏问题都能在这条链路上提前发现。想继续深入启动耗时与卡顿定位,可以看 鸿蒙性能调优与调试 ;如果初始化里有耗时任务需要异步化,并发与 TaskPool 那一篇给了完整的落地方案。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。