HarmonyOS 6.0 页面生命周期与状态恢复
HarmonyOS的页面生命周期比Android简单——没有onStart/onStop/onResume那堆回调,但简单不等于没有坑。UIAbility的生命周期、NavDestination的显示/隐藏、组件的aboutToAppear/aboutToDisappear,这三层生命周期的执行顺序和交互逻辑如果不搞清楚,状态管理就会一团糟。加上应用切后台被系统回收后恢复状态的需求,生命周期的问题远比想象中复杂。
UIAbility生命周期

UIAbility是应用的入口组件,生命周期回调:
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 应用创建,初始化全局数据
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// 窗口创建,加载主页面
windowStage.loadContent('pages/Index');
}
onForeground(): void {
// 应用回到前台
}
onBackground(): void {
// 应用切到后台
}
onWindowStageDestroy(): void {
// 窗口销毁
}
onDestroy(): void {
// 应用销毁
}
}
执行顺序:onCreate → onWindowStageCreate → onForeground → (运行中) → onBackground → (后台) → onForeground → … → onBackground → onWindowStageDestroy → onDestroy
关键:onBackground不是onDestroy。 应用切后台后可能长时间存活,也可能随时被系统杀掉。不要在onBackground中做"最终清理"——那是onDestroy的事。
NavDestination生命周期
NavDestination有自己的显示/隐藏回调:
NavDestination() {
// 页面内容
}
.onShown(() => {
// 页面变为可见(push到栈顶或返回到此页)
})
.onHidden(() => {
// 页面被覆盖(新页面push或本页pop)
})
.onWillAppear(() => {
// 页面即将显示,动画开始前
})
.onWillDisappear(() => {
// 页面即将消失,动画开始前
})
.onDisappeared(() => {
// 页面已从导航栈移除
})
执行顺序:push新页面时,新页面的onWillAppear → onShown,旧页面的onHidden。pop返回时,当前页的onWillDisappear,下级页面的onShown。
onShown/onHidden可以反复触发——push覆盖触发onHidden,pop回来触发onShown。aboutToAppear只在组件首次创建时触发一次。
组件生命周期
@Component
struct MyComponent {
aboutToAppear(): void {
// 组件即将构建,在build之前
// 适合做数据初始化
}
aboutToDisappear(): void {
// 组件即将销毁
// 适合做资源清理
}
build() {
// 构建UI
}
}
aboutToAppear中不要做耗时操作。 它在UI线程执行,如果这里做网络请求或大量计算,会阻塞首帧渲染。正确做法是aboutToAppear中发起异步请求,UI先显示加载态,数据回来后更新。
三层生命周期协作
UIAbility、NavDestination、Component三层生命周期的执行时序:
应用启动:
UIAbility.onCreate → UIAbility.onWindowStageCreate → UIAbility.onForeground
→ Index页面 aboutToAppear → Index页面 build → Index页面 onShown
push到详情页:
Detail页面 aboutToAppear → Detail页面 build
→ Index页面 onHidden → Detail页面 onShown
pop返回:
Detail页面 onWillDisappear → Detail页面 aboutToDisappear
→ Index页面 onShown
应用切后台:
UIAbility.onBackground → 所有页面保持不变(组件不销毁)
应用回前台:
UIAbility.onForeground
应用被杀:
无回调,下次启动走onCreate
最大的坑:应用切后台再回来,组件不重新创建。 aboutToAppear不会再次触发。如果页面数据可能过期(比如切后台超过5分钟),需要在onForeground或onShown中刷新。
后台恢复策略
应用在后台可能被系统回收。回来时如果进程还在,数据还在内存中;如果进程被杀,需要从冷启动恢复。
判断是否从冷启动恢复:
class EntryAbility extends UIAbility {
private isColdStart: boolean = true;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (launchParam.launchReason === AbilityConstant.LaunchReason.STARTUP_CONTINUATION) {
// 从异常恢复启动
this.isColdStart = false;
} else {
this.isColdStart = true;
}
}
}
STARTUP_CONTINUATION表示应用从异常终止后恢复。这个场景需要从持久化存储中恢复数据。
关键状态持久化
切后台前保存关键数据,回来后恢复:
import { preferences } from '@kit.ArkData';
// 保存关键状态
async saveAppState(): Promise<void> {
let pref = await preferences.getPreferences(this.context, 'app_state');
await pref.putSync('last_active_time', Date.now());
await pref.putSync('current_page', this.currentPageName);
await pref.putSync('scroll_position', this.scrollOffset);
await pref.putSync('form_draft', this.draftContent);
await pref.flush();
}
// 恢复关键状态
async restoreAppState(): Promise<void> {
let pref = await preferences.getPreferences(this.context, 'app_state');
let lastTime = pref.getSync('last_active_time', 0) as number;
let elapsed = Date.now() - lastTime;
if (elapsed > 5 * 60 * 1000) {
// 超过5分钟,数据可能过期,重新加载
this.refreshAllData();
} else {
// 5分钟内,恢复保存的状态
this.scrollOffset = pref.getSync('scroll_position', 0) as number;
this.draftContent = pref.getSync('form_draft', '') as string;
}
}
在onBackground中调用saveAppState,在onForeground中调用restoreAppState。超过5分钟的数据视为过期,强制刷新。
不要保存所有状态——只保存用户自己输入的、不可重新获取的数据。 比如表单草稿、未发送的消息、编辑中的文本。列表数据、详情数据可以从网络重新加载,不需要持久化。
滚动位置恢复
列表页跳详情再返回,滚动位置丢失是最常见的体验问题:
private scroller: Scroller = new Scroller();
private savedScrollOffset: number = 0;
List() {
// 内容
}
.scroller(this.scroller)
.onScroll(() => {
this.savedScrollOffset = this.scroller.currentOffset().yOffset;
})
// 返回时恢复
onShown(): void {
if (this.savedScrollOffset > 0) {
setTimeout(() => {
this.scroller.scrollTo({ xOffset: 0, yOffset: this.savedScrollOffset });
}, 50);
}
}
onScroll中持续记录偏移量。返回时scrollTo恢复。50ms延迟等列表渲染完成。
问题:如果返回时列表数据变化了(比如删除了一条),滚动位置可能对不上。 精确恢复需要记录第一个可见项的id而非偏移量:
private firstVisibleId: string = '';
.onScrollIndex((start: number) => {
if (start < this.items.length) {
this.firstVisibleId = this.items[start].id;
}
})
// 恢复
onShown(): void {
let targetIndex = -1;
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].id === this.firstVisibleId) {
targetIndex = i;
break;
}
}
if (targetIndex >= 0) {
setTimeout(() => {
this.scroller.scrollToIndex(targetIndex);
}, 50);
}
}
用id找index,即使列表项有增删也能恢复到正确位置。
表单数据保护
用户填了一半表单,跳到别的页面再回来,输入内容丢失:
@State formDraft: string = ''
private autoSaveTimer: number = -1
TextArea({ text: $$this.formDraft })
.width('100%')
.height(120)
.onChange(() => {
this.scheduleAutoSave();
})
private scheduleAutoSave(): void {
if (this.autoSaveTimer !== -1) {
clearTimeout(this.autoSaveTimer);
}
this.autoSaveTimer = setTimeout(() => {
this.saveDraft();
}, 1000);
}
private async saveDraft(): Promise<void> {
let pref = await preferences.getPreferences(this.context, 'drafts');
await pref.putSync('draft_' + this.pageId, this.formDraft);
await pref.flush();
}
输入变化后1秒自动保存草稿(防抖)。回到页面时从Preferences读取草稿:
aboutToAppear(): void {
this.loadDraft();
}
private async loadDraft(): Promise<void> {
let pref = await preferences.getPreferences(this.context, 'drafts');
this.formDraft = pref.getSync('draft_' + this.pageId, '') as string;
}
草稿在提交成功后清除:
async submitForm(): Promise<void> {
await this.api.submit(this.formDraft);
let pref = await preferences.getPreferences(this.context, 'drafts');
await pref.deleteSync('draft_' + this.pageId);
await pref.flush();
this.formDraft = '';
}
定时器与监听器清理
组件销毁时必须清理所有异步资源:
private timers: number[] = [];
private emitterCallbacks: emitter.Callback[] = [];
aboutToAppear(): void {
let t1 = setInterval(() => { this.tick(); }, 1000);
this.timers.push(t1);
let callback = (data: emitter.EventData) => { this.onEvent(data); };
this.emitterCallbacks.push(callback);
emitter.on({ eventId: 1001 }, callback);
}
aboutToDisappear(): void {
for (let i = 0; i < this.timers.length; i++) {
clearInterval(this.timers[i]);
}
for (let i = 0; i < this.emitterCallbacks.length; i++) {
emitter.off(1001, this.emitterCallbacks[i]);
}
}
规则:在aboutToAppear中创建的定时器、监听器、订阅,必须在aboutToDisappear中清理。 每加一个setInterval/emitter.on/display.on,就加对应的清理代码。不多不少。
踩坑清单
| 问题 | 原因 | 解决 |
|---|---|---|
| 切后台回来数据没刷新 | aboutToAppear不再次触发 | 在onForeground/onShown中刷新 |
| 滚动位置丢失 | 没记录偏移量 | scroller.currentOffset + scrollTo |
| 表单内容丢失 | 组件重建了 | 草稿持久化到Preferences |
| 定时器泄漏 | aboutToDisappear没清理 | 每个setInterval对应clearInterval |
| 页面重复初始化 | onShown和aboutToAppear都做了初始化 | 初始化放aboutToAppear,刷新放onShown |
| 进程被杀后状态丢失 | 内存数据没持久化 | 关键数据写Preferences |
| onHidden中做清理 | onHidden≠销毁,还可能onShown回来 | 清理放aboutToDisappear |
| 冷启动没恢复数据 | 没判断launchReason | 检查STARTUP_CONTINUATION |
| scrollTo时机太早 | 列表还没渲染完 | setTimeout 50ms延迟 |
| 多次注册监听器 | 每次onShown都on | on只注册一次,放aboutToAppear |
生命周期管理的核心原则:创建和销毁成对,显示和刷新分离,关键数据持久化。 aboutToAppear做初始化,aboutToDisappear做清理,onShown做刷新,onBackground做保存。搞清楚每层回调的触发时机和生命周期,状态管理就不会乱。
更多推荐
所有评论(0)