UIAbility 冷启动、热启动与重复拉起处理:把入口状态写稳
UIAbility 冷启动、热启动与重复拉起处理把入口状态写稳应用的启动问题不是“打不开”而是偶发地打开错页面、重复创建页面、通知带来的参数丢失或者用户从桌面再次进入时看到旧状态。冷启动、热启动、重复拉起看起来都是“启动应用”但系统回调、数据来源和页面恢复策略完全不同。这篇用一个“消息详情 首页 通知点击”的场景说明应用第一次启动、后台返回、同一个 Ability 被再次拉起时代码应该分别放在哪里。重点不是背生命周期而是建立一条稳定的入口链路。1. 先把三种启动入口分清楚冷启动指进程和 Ability 都需要重新创建系统会走onCreate页面也要重新加载。热启动通常是应用还在后台用户重新回到前台重点在状态刷新而不是重新建页面。重复拉起是已有 Ability 实例再次收到新的Want典型回调是onNewWant。入口类型典型触发关键回调工程重点冷启动桌面图标、通知首次打开onCreate解析初始 Want准备首页路由热启动最近任务、后台回前台onForeground刷新可见数据避免重复初始化重复拉起singleton 实例再次被启动onNewWant合并新参数跳转到目标页面exportenumEntrySource{Launcherlauncher,Notificationnotification,DeepLinkdeepLink,Unknownunknown}exportinterfaceEntryIntent{source:EntrySource;targetPage:string;messageId?:string;}代码解释入口来源先枚举化后面日志、路由和埋点才能统一。targetPage不直接写页面路径常量到 Ability 中避免入口层和页面层互相依赖。messageId是业务参数必须允许为空因为桌面启动没有这个字段。2. 冷启动只做一次入口解析冷启动阶段最容易写乱有人在onCreate里初始化数据库、加载用户信息、跳转页面、请求接口最后启动耗时变长还难以排查。更稳的做法是onCreate只把Want解析成业务入口对象然后交给路由协调器。import{UIAbility,Want}fromkit.AbilityKit;import{hilog}fromkit.PerformanceAnalysisKit;constDOMAIN0x20260702;exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want):void{constintentEntryIntentParser.fromWant(want);hilog.info(DOMAIN,EntryAbility,cold start target%{public}s,intent.targetPage);AppEntryDispatcher.cacheInitialIntent(intent);}}代码解释onCreate不直接跳页面因为窗口还没有完成加载。初始入口先缓存等onWindowStageCreate加载首页后再消费。日志只打公开字段业务敏感参数不要直接输出。3. 窗口创建后再做首屏路由WindowStage创建完成后页面容器才具备加载能力。这个阶段适合加载首页然后把冷启动解析出来的入口交给首页路由处理。import{window}fromkit.ArkUI;exportdefaultclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.loadContent(pages/Index,(error){if(error.code){hilog.error(DOMAIN,EntryAbility,load page failed%{public}d,error.code);return;}constintentAppEntryDispatcher.takeInitialIntent();if(intent){AppEntryDispatcher.dispatch(intent);}});}}代码解释loadContent成功后再处理业务跳转能避免页面未就绪时路由失败。takeInitialIntent读取后清空避免冷启动入口被重复消费。如果首页加载失败不能继续执行业务跳转否则只会制造二次异常。4. 热启动不要重复做冷启动初始化热启动时用户通常希望看到最近状态但也可能需要刷新登录态、消息红点或权限状态。这里的关键是“刷新可见状态”不是把冷启动流程再执行一遍。exportclassForegroundRefreshPolicy{privatestaticlastRefreshAt0;staticshouldRefresh(now:number):boolean{constintervalnow-ForegroundRefreshPolicy.lastRefreshAt;if(interval15_000){returnfalse;}ForegroundRefreshPolicy.lastRefreshAtnow;returntrue;}}exportdefaultclassEntryAbilityextendsUIAbility{onForeground():void{if(ForegroundRefreshPolicy.shouldRefresh(Date.now())){AppEntryDispatcher.refreshVisibleState();}}}代码解释加入简单节流避免用户频繁切换前后台时反复请求。refreshVisibleState只刷新可见信息不重建页面栈。这个策略适合红点、权限、网络状态不适合重放通知跳转。5. 重复拉起必须处理 onNewWant如果launchType是singleton同一个 Ability 被再次启动时系统可能不会重新创建实例而是通过onNewWant传入新的Want。通知点击、外部链接、桌面快捷方式都可能走到这里。exportdefaultclassEntryAbilityextendsUIAbility{onNewWant(want:Want):void{constintentEntryIntentParser.fromWant(want);hilog.info(DOMAIN,EntryAbility,new want target%{public}s,intent.targetPage);AppEntryDispatcher.dispatch(intent);}}代码解释onNewWant不能空着否则重复拉起参数会被直接丢弃。解析逻辑复用EntryIntentParser避免冷启动和热入口判断不一致。这里通常不需要重新loadContent而是通知页面层执行跳转或刷新。6. 写一个可测试的 Want 解析器入口解析不要散落在 Ability、页面和通知模块里。把解析集中成一个纯函数后面做单元测试和问题复盘都更方便。import{Want}fromkit.AbilityKit;exportclassEntryIntentParser{staticfromWant(want?:Want):EntryIntent{constparamswant?.parameters??{};constsourceString(params[source]??EntrySource.Unknown);constmessageIdparams[messageId]?String(params[messageId]):undefined;if(messageId){return{source:sourceasEntrySource,targetPage:MessageDetail,messageId};}return{source:EntrySource.Launcher,targetPage:Home};}}代码解释parameters统一做空值兜底避免通知参数缺失导致异常。有messageId才进入详情页没有就回首页。解析器返回业务对象不返回 UI 路径这样页面结构调整时入口层不用大改。7. 路由分发要做幂等保护重复拉起最怕同一条消息连续触发两次页面栈里出现两个详情页。分发器可以记录最近处理的入口 key对短时间重复请求直接忽略。exportclassAppEntryDispatcher{privatestaticinitialIntent?:EntryIntent;privatestaticlastKey;staticcacheInitialIntent(intent:EntryIntent):void{AppEntryDispatcher.initialIntentintent;}statictakeInitialIntent():EntryIntent|undefined{constintentAppEntryDispatcher.initialIntent;AppEntryDispatcher.initialIntentundefined;returnintent;}staticdispatch(intent:EntryIntent):void{constkey${intent.targetPage}:${intent.messageId??default};if(keyAppEntryDispatcher.lastKey){return;}AppEntryDispatcher.lastKeykey;RouterBridge.open(intent);}}代码解释冷启动入口用一次后清空避免首页恢复时再次跳转。lastKey能挡住短时间重复点击通知造成的重复导航。真正页面跳转放到RouterBridgeAbility 层不直接依赖 ArkUI 页面实现。8. 验证时不要只看“能打开”启动链路要用日志和测试用例验证。至少覆盖桌面冷启动、通知冷启动、后台通知拉起、最近任务返回、连续点击同一通知五种情况。exportconstlaunchCases[{name:桌面冷启动,source:launcher,expected:Home},{name:通知冷启动,source:notification,messageId:1001,expected:MessageDetail},{name:后台通知拉起,source:notification,messageId:1002,expected:MessageDetail},{name:最近任务返回,source:recent,expected:KeepCurrentPage},{name:重复点击通知,source:notification,messageId:1002,expected:NoDuplicatePage}];代码解释用表格化用例描述启动行为比口头说明更容易复现。KeepCurrentPage表示热启动不能破坏当前页面栈。NoDuplicatePage是重复拉起的关键验收点。9. 常见问题排查清单现象高概率原因排查位置通知点击无反应onNewWant没处理EntryAbility首次打开白屏后跳转失败loadContent前就路由WindowStage回到前台重复请求接口热启动没有节流onForeground同一详情页打开两份分发器缺少幂等 keyDispatcher参数偶发为空Want 解析散落多处Parser10. 小结冷启动、热启动、重复拉起不是三个孤立概念而是一条完整入口链路。工程上建议按“Ability 解析入口、WindowStage 加载容器、Dispatcher 分发业务、页面层执行展示”的顺序拆开。只要onCreate、onWindowStageCreate、onForeground、onNewWant的职责清楚启动问题就不会随着页面数量增加而失控。