UIAbility 冷启动、热启动与重复拉起处理:把入口状态写稳
UIAbility 冷启动、热启动与重复拉起处理:把入口状态写稳
应用的启动问题不是“打不开”,而是偶发地打开错页面、重复创建页面、通知带来的参数丢失,或者用户从桌面再次进入时看到旧状态。冷启动、热启动、重复拉起看起来都是“启动应用”,但系统回调、数据来源和页面恢复策略完全不同。
这篇用一个“消息详情 + 首页 + 通知点击”的场景说明:应用第一次启动、后台返回、同一个 Ability 被再次拉起时,代码应该分别放在哪里。重点不是背生命周期,而是建立一条稳定的入口链路。
1. 先把三种启动入口分清楚
冷启动指进程和 Ability 都需要重新创建,系统会走onCreate,页面也要重新加载。热启动通常是应用还在后台,用户重新回到前台,重点在状态刷新而不是重新建页面。重复拉起是已有 Ability 实例再次收到新的Want,典型回调是onNewWant。
| 入口类型 | 典型触发 | 关键回调 | 工程重点 |
|---|---|---|---|
| 冷启动 | 桌面图标、通知首次打开 | onCreate | 解析初始 Want,准备首页路由 |
| 热启动 | 最近任务、后台回前台 | onForeground | 刷新可见数据,避免重复初始化 |
| 重复拉起 | singleton 实例再次被启动 | onNewWant | 合并新参数,跳转到目标页面 |
exportenumEntrySource{Launcher='launcher',Notification='notification',DeepLink='deepLink',Unknown='unknown'}exportinterfaceEntryIntent{source:EntrySource;targetPage:string;messageId?:string;}代码解释:
- 入口来源先枚举化,后面日志、路由和埋点才能统一。
targetPage不直接写页面路径常量到 Ability 中,避免入口层和页面层互相依赖。messageId是业务参数,必须允许为空,因为桌面启动没有这个字段。
2. 冷启动只做一次入口解析
冷启动阶段最容易写乱:有人在onCreate里初始化数据库、加载用户信息、跳转页面、请求接口,最后启动耗时变长,还难以排查。更稳的做法是onCreate只把Want解析成业务入口对象,然后交给路由协调器。
import{UIAbility,Want}from'@kit.AbilityKit';import{hilog}from'@kit.PerformanceAnalysisKit';constDOMAIN=0x20260702;exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want):void{constintent=EntryIntentParser.fromWant(want);hilog.info(DOMAIN,'EntryAbility','cold start target=%{public}s',intent.targetPage);AppEntryDispatcher.cacheInitialIntent(intent);}}代码解释:
onCreate不直接跳页面,因为窗口还没有完成加载。- 初始入口先缓存,等
onWindowStageCreate加载首页后再消费。 - 日志只打公开字段,业务敏感参数不要直接输出。
3. 窗口创建后再做首屏路由
WindowStage创建完成后,页面容器才具备加载能力。这个阶段适合加载首页,然后把冷启动解析出来的入口交给首页路由处理。
import{window}from'@kit.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;}constintent=AppEntryDispatcher.takeInitialIntent();if(intent){AppEntryDispatcher.dispatch(intent);}});}}代码解释:
loadContent成功后再处理业务跳转,能避免页面未就绪时路由失败。takeInitialIntent读取后清空,避免冷启动入口被重复消费。- 如果首页加载失败,不能继续执行业务跳转,否则只会制造二次异常。
4. 热启动不要重复做冷启动初始化
热启动时用户通常希望看到最近状态,但也可能需要刷新登录态、消息红点或权限状态。这里的关键是“刷新可见状态”,不是把冷启动流程再执行一遍。
exportclassForegroundRefreshPolicy{privatestaticlastRefreshAt=0;staticshouldRefresh(now:number):boolean{constinterval=now-ForegroundRefreshPolicy.lastRefreshAt;if(interval<15_000){returnfalse;}ForegroundRefreshPolicy.lastRefreshAt=now;returntrue;}}exportdefaultclassEntryAbilityextendsUIAbility{onForeground():void{if(ForegroundRefreshPolicy.shouldRefresh(Date.now())){AppEntryDispatcher.refreshVisibleState();}}}代码解释:
- 加入简单节流,避免用户频繁切换前后台时反复请求。
refreshVisibleState只刷新可见信息,不重建页面栈。- 这个策略适合红点、权限、网络状态,不适合重放通知跳转。
5. 重复拉起必须处理 onNewWant
如果launchType是singleton,同一个 Ability 被再次启动时,系统可能不会重新创建实例,而是通过onNewWant传入新的Want。通知点击、外部链接、桌面快捷方式都可能走到这里。
exportdefaultclassEntryAbilityextendsUIAbility{onNewWant(want:Want):void{constintent=EntryIntentParser.fromWant(want);hilog.info(DOMAIN,'EntryAbility','new want target=%{public}s',intent.targetPage);AppEntryDispatcher.dispatch(intent);}}代码解释:
onNewWant不能空着,否则重复拉起参数会被直接丢弃。- 解析逻辑复用
EntryIntentParser,避免冷启动和热入口判断不一致。 - 这里通常不需要重新
loadContent,而是通知页面层执行跳转或刷新。
6. 写一个可测试的 Want 解析器
入口解析不要散落在 Ability、页面和通知模块里。把解析集中成一个纯函数,后面做单元测试和问题复盘都更方便。
import{Want}from'@kit.AbilityKit';exportclassEntryIntentParser{staticfromWant(want?:Want):EntryIntent{constparams=want?.parameters??{};constsource=String(params['source']??EntrySource.Unknown);constmessageId=params['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.initialIntent=intent;}statictakeInitialIntent():EntryIntent|undefined{constintent=AppEntryDispatcher.initialIntent;AppEntryDispatcher.initialIntent=undefined;returnintent;}staticdispatch(intent:EntryIntent):void{constkey=`${intent.targetPage}:${intent.messageId??'default'}`;if(key===AppEntryDispatcher.lastKey){return;}AppEntryDispatcher.lastKey=key;RouterBridge.open(intent);}}代码解释:
- 冷启动入口用一次后清空,避免首页恢复时再次跳转。
lastKey能挡住短时间重复点击通知造成的重复导航。- 真正页面跳转放到
RouterBridge,Ability 层不直接依赖 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 |
| 同一详情页打开两份 | 分发器缺少幂等 key | Dispatcher |
| 参数偶发为空 | Want 解析散落多处 | Parser |
10. 小结
冷启动、热启动、重复拉起不是三个孤立概念,而是一条完整入口链路。工程上建议按“Ability 解析入口、WindowStage 加载容器、Dispatcher 分发业务、页面层执行展示”的顺序拆开。只要onCreate、onWindowStageCreate、onForeground、onNewWant的职责清楚,启动问题就不会随着页面数量增加而失控。