HarmonyOS的页面生命周期比Android简单——没有onStart/onStop/onResume那堆回调,但简单不等于没有坑。UIAbility的生命周期、NavDestination的显示/隐藏、组件的aboutToAppear/aboutToDisappear,这三层生命周期的执行顺序和交互逻辑如果不搞清楚,状态管理就会一团糟。加上应用切后台被系统回收后恢复状态的需求,生命周期的问题远比想象中复杂。
UIAbility生命周期
UIAbility是应用的入口组件,生命周期回调:
import{UIAbility,AbilityConstant,Want}from'@kit.AbilityKit';import{window}from'@kit.ArkUI';classEntryAbilityextendsUIAbility{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只在组件首次创建时触发一次。
组件生命周期
@Componentstruct 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中刷新。
后台恢复策略
应用在后台可能被系统回收。回来时如果进程还在,数据还在内存中;如果进程被杀,需要从冷启动恢复。
判断是否从冷启动恢复:
classEntryAbilityextendsUIAbility{privateisColdStart: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';// 保存关键状态asyncsaveAppState():Promise<void>{letpref=awaitpreferences.getPreferences(this.context,'app_state');awaitpref.putSync('last_active_time',Date.now());awaitpref.putSync('current_page',this.currentPageName);awaitpref.putSync('scroll_position',this.scrollOffset);awaitpref.putSync('form_draft',this.draftContent);awaitpref.flush();}// 恢复关键状态asyncrestoreAppState():Promise<void>{letpref=awaitpreferences.getPreferences(this.context,'app_state');letlastTime=pref.getSync('last_active_time',0)asnumber;letelapsed=Date.now()-lastTime;if(elapsed>5*60*1000){// 超过5分钟,数据可能过期,重新加载this.refreshAllData();}else{// 5分钟内,恢复保存的状态this.scrollOffset=pref.getSync('scroll_position',0)asnumber;this.draftContent=pref.getSync('form_draft','')asstring;}}在onBackground中调用saveAppState,在onForeground中调用restoreAppState。超过5分钟的数据视为过期,强制刷新。
不要保存所有状态——只保存用户自己输入的、不可重新获取的数据。比如表单草稿、未发送的消息、编辑中的文本。列表数据、详情数据可以从网络重新加载,不需要持久化。
滚动位置恢复
列表页跳详情再返回,滚动位置丢失是最常见的体验问题:
privatescroller:Scroller=newScroller();privatesavedScrollOffset: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而非偏移量:
privatefirstVisibleId:string='';.onScrollIndex((start:number)=>{if(start<this.items.length){this.firstVisibleId=this.items[start].id;}})// 恢复onShown():void{lettargetIndex=-1;for(leti=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,即使列表项有增删也能恢复到正确位置。
表单数据保护
用户填了一半表单,跳到别的页面再回来,输入内容丢失:
@StateformDraft:string=''privateautoSaveTimer:number=-1TextArea({text:$$this.formDraft}).width('100%').height(120).onChange(()=>{this.scheduleAutoSave();})privatescheduleAutoSave():void{if(this.autoSaveTimer!==-1){clearTimeout(this.autoSaveTimer);}this.autoSaveTimer=setTimeout(()=>{this.saveDraft();},1000);}privateasyncsaveDraft():Promise<void>{letpref=awaitpreferences.getPreferences(this.context,'drafts');awaitpref.putSync('draft_'+this.pageId,this.formDraft);awaitpref.flush();}输入变化后1秒自动保存草稿(防抖)。回到页面时从Preferences读取草稿:
aboutToAppear():void{this.loadDraft();}privateasyncloadDraft():Promise<void>{letpref=awaitpreferences.getPreferences(this.context,'drafts');this.formDraft=pref.getSync('draft_'+this.pageId,'')asstring;}草稿在提交成功后清除:
asyncsubmitForm():Promise<void>{awaitthis.api.submit(this.formDraft);letpref=awaitpreferences.getPreferences(this.context,'drafts');awaitpref.deleteSync('draft_'+this.pageId);awaitpref.flush();this.formDraft='';}定时器与监听器清理
组件销毁时必须清理所有异步资源:
privatetimers:number[]=[];privateemitterCallbacks:emitter.Callback[]=[];aboutToAppear():void{lett1=setInterval(()=>{this.tick();},1000);this.timers.push(t1);letcallback=(data:emitter.EventData)=>{this.onEvent(data);};this.emitterCallbacks.push(callback);emitter.on({eventId:1001},callback);}aboutToDisappear():void{for(leti=0;i<this.timers.length;i++){clearInterval(this.timers[i]);}for(leti=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做保存。搞清楚每层回调的触发时机和生命周期,状态管理就不会乱。