UIAbility 启动模式实战:singleton、multiton、specified 到底怎么选
很多 HarmonyOS 应用一开始只有一个首页,默认配置也能跑起来。问题通常出现在第二阶段:通知点击要打开详情页,外部应用要拉起某个业务页面,用户连续打开多个编辑任务,或者同一个文档被重复点开。这时如果还把所有入口都当成普通页面跳转处理,页面栈很快会变乱。
UIAbility 的启动模式解决的就是“实例怎么管理”的问题。它不是一个背概念的配置项,而是决定系统再次启动同一个 Ability 时,是复用旧实例、创建新实例,还是按业务 key 找到指定实例。
本文用一个文档编辑类应用做例子,讲清楚三件事:
- 首页入口为什么适合
singleton。 - 多任务场景为什么可能需要
multiton。 - 文档编辑为什么更适合
specified。 onNewWant应该怎么处理新参数。- 如何用日志验证启动模式真的生效。
1. 先用业务场景选择启动模式
启动模式不要凭感觉选。更稳的方式是先问业务问题:同一个入口被再次拉起时,用户希望看到原来的实例,还是希望新开一个实例?
比如首页通常只有一个,重复点击桌面图标应该回到已有首页,这类入口适合singleton。文档编辑则不同,A 文档和 B 文档应该互不影响,但重复打开 A 文档又应该回到已有 A 文档,这类场景更适合specified。如果每次打开都需要完全独立,比如临时预览任务,就可以考虑multiton。
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 首页、主入口 | singleton | 保持主入口唯一,重复启动走onNewWant |
| 临时详情、一次性任务 | multiton | 每次启动可以创建独立实例 |
| 文档、会话、编辑器 | specified | 按业务 key 复用指定实例 |
exporttypeLaunchScene='home'|'preview'|'document';exporttypeLaunchType='singleton'|'multiton'|'specified';exportfunctionchooseLaunchType(scene:LaunchScene):LaunchType{if(scene==='home'){return'singleton';}if(scene==='document'){return'specified';}return'multiton';}代码解释:
- 这段代码不是系统 API,而是把团队的启动模式选择规则写清楚。
- 项目评审时可以先看这个规则,再决定
module.json5怎么配置。 - 避免每个开发者凭经验选择启动模式,后期维护成本会低很多。
2. 在 module.json5 中配置 launchType
启动模式最终要落到module.json5。如果这里只配置错了,ArkTS 代码写得再完整,系统实例选择也不会按预期执行。
下面示例把首页配置为singleton,把文档编辑 Ability 配置为specified:
{ "module": { "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "launchType": "singleton", "exported": true }, { "name": "DocumentAbility", "srcEntry": "./ets/documentability/DocumentAbility.ets", "launchType": "specified", "exported": false } ] } }代码解释:
EntryAbility作为主入口,重复启动时复用已有实例。DocumentAbility用specified,为后续按文档 id 复用实例做准备。- 修改
module.json5后要重新构建安装,不能只看页面热更新效果。
3. singleton 的重点是 onNewWant
singleton不是“只启动一次就完事”。它真正需要处理的是:旧实例还在时,新 Want 进来怎么办?
如果没有处理onNewWant,用户从通知点击进入详情页时,系统可能已经把参数交给了旧实例,但你的页面没有任何变化,于是看起来像“点击无效”。
import{UIAbility,Want}from'@kit.AbilityKit';import{hilog}from'@kit.PerformanceAnalysisKit';constDOMAIN=0x0000;exportdefaultclassEntryAbilityextendsUIAbility{onNewWant(want:Want):void{hilog.info(DOMAIN,'EntryAbility','onNewWant received');EntryRouteDispatcher.dispatch(want);}}classEntryRouteDispatcher{staticdispatch(want:Want):void{constpage=want.parameters?.pageasstring??'Index';constid=want.parameters?.idasstring??'';hilog.info(DOMAIN,'EntryRoute','page=%{public}s id=%{public}s',page,id);}}代码解释:
onNewWant专门处理复用实例收到的新参数。- 不建议在
onNewWant里直接写复杂页面逻辑,最好交给路由分发类。 - 日志里打印
page和id,后续排查通知、外链、跨应用启动会方便很多。
4. multiton 适合独立任务,不适合普通详情页滥用
multiton可以让同一个 Ability 创建多个实例,但这不代表它适合所有详情页。实例多了以后,页面栈、内存占用、返回路径和状态同步都会变复杂。
适合使用multiton的场景通常有两个特征:
- 每个任务之间确实独立。
- 用户需要同时保留多个任务上下文。
exportinterfaceAbilityTaskSnapshot{instanceId:string;title:string;pageStack:string[];updatedAt:number;}exportclassAbilityTaskStore{privatestaticsnapshots:AbilityTaskSnapshot[]=[];staticadd(snapshot:AbilityTaskSnapshot):void{AbilityTaskStore.snapshots.push(snapshot);}staticlist():AbilityTaskSnapshot[]{returnAbilityTaskStore.snapshots;}}代码解释:
- 多实例场景要能记录每个实例的业务快照。
pageStack用来辅助排查返回路径是否符合预期。- 如果你不需要记录这些状态,大概率也不需要
multiton。
5. specified 的核心是实例 key
specified的价值在于“按业务 key 复用实例”。以文档编辑为例,同一个用户打开同一个文档,应当回到已有编辑实例;不同文档则应该互相隔离。
实例 key 不要随手拼。建议明确包含哪些字段,例如用户 id、文档 id、租户 id。字段太少会误复用,字段太多又会导致本该复用的实例无法命中。
import{Want}from'@kit.AbilityKit';exportclassDocumentInstanceKeyFactory{staticcreate(want:Want):string{constuserId=want.parameters?.userIdasstring??'guest';constdocumentId=want.parameters?.documentIdasstring??'';if(!documentId){return`${userId}:empty`;}return`${userId}:${documentId}`;}}代码解释:
userId + documentId可以避免不同用户之间错误复用实例。documentId缺失时要有兜底,避免生成不可预测的 key。- 真实项目中可以继续加入租户、空间、版本等字段。
6. 把 Want 转成业务路由对象
Ability 收到的是 Want,但页面不应该直接依赖 Want。页面更适合接收业务路由对象,比如目标页面、业务 id、来源类型。
这样做的好处是:启动来源可以变化,但页面接收的数据结构稳定。
import{Want}from'@kit.AbilityKit';exportinterfaceRouteRequest{targetPage:string;bizId:string;source:string;}exportclassAbilityRouteParser{staticparse(want:Want):RouteRequest{return{targetPage:want.parameters?.pageasstring??'Index',bizId:want.parameters?.idasstring??'',source:want.parameters?.sourceasstring??'unknown'};}}代码解释:
RouteRequest是页面和 Ability 之间的稳定协议。source用于区分桌面、通知、外部应用或内部跳转。- 默认值能避免参数缺失时页面直接异常。
7. 运行验证要覆盖三条路径
启动模式是否正确,不能只启动一次就下结论。至少要验证三条路径:
| 验证路径 | 预期结果 | 重点日志 |
|---|---|---|
| 首次打开应用 | 走onCreate和窗口加载 | onCreate、loadContent |
| 已有实例再次拉起 | 走onNewWant | onNewWant、路由参数 |
| 不同文档 id 拉起 | specified key 不同 | instanceKey |
import{hilog}from'@kit.PerformanceAnalysisKit';constDOMAIN=0x0000;exportclassLaunchModeVerifier{staticmark(stage:string,source:string,bizId:string):void{hilog.info(DOMAIN,'LaunchModeVerifier','stage=%{public}s source=%{public}s bizId=%{public}s',stage,source,bizId);}}代码解释:
stage记录当前处于onCreate、onNewWant还是页面路由。source记录启动来源,方便区分桌面、通知和外部调用。bizId用来验证 specified key 是否符合预期。
8. 常见错误和修复建议
| 问题 | 典型表现 | 修复建议 |
|---|---|---|
忘记配置launchType | 行为和预期不一致 | 回到module.json5检查 Ability 声明 |
singleton 没处理onNewWant | 重复点击没有页面变化 | 在onNewWant中解析新 Want |
| specified key 不稳定 | 该复用时没复用 | 明确 key 字段,避免随机值 |
| 页面直接读 Want | 页面和系统入口耦合 | 先转换成RouteRequest |
exportconstLaunchModeChecklist:string[]=['module.json5 是否配置 launchType','singleton 是否处理 onNewWant','specified 是否有稳定 instance key','Want 是否转换成业务 RouteRequest','日志是否覆盖 onCreate、onNewWant 和路由分发'];代码解释:
- 这份清单适合放到团队 code review 模板里。
- 每新增一个 Ability 或外部入口,都应该按清单检查。
- 如果列表里有任何一项不满足,先修启动链路,再继续写页面。
9. 总结
UIAbility 启动模式真正解决的是实例管理问题。singleton关注复用旧实例后的新参数处理,multiton关注多个独立任务的状态隔离,specified关注按业务 key 精准复用实例。
写项目时建议按这个顺序落地:
- 先判断业务是否需要复用实例。
- 再配置
module.json5的launchType。 - 然后处理
onCreate和onNewWant。 - 最后把 Want 转成业务路由对象,并用日志验证完整链路。
参考资料
- 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/abilitystage
- 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/stage-model-development-overview
- 华为开发者文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/want-overview