
你好欢迎来到我的博客我是【菜鸟学鸿蒙】我是一名在路上的移动端开发者正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来也为了和更多同路人互相启发我决定把探索 HarmonyOS 的过程都记录在这里。️主要方向ArkTS 语言基础、HarmonyOS 原生应用Stage 模型、UIAbility/ServiceAbility、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战以及 Android → 鸿蒙的迁移踩坑与复盘。内容节奏从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘让每篇都有可落地的代码与方法论。 我相信写作是把知识内化的过程分享是让生态更繁荣的方式。如果你也想拥抱鸿蒙、热爱成长欢迎关注我一起交流进步前言一个“详情页越点越多”的问题不一定出在 ArkUI 路由。在 Stage 模型里如果业务实际上启动的是新的 UIAbility那么实例是否复用首先受module.json5中launchType控制。singleton、multiton和specified三种模式对“第二次启动同一个 UIAbility 时到底发生什么”给出了完全不同的答案。这次不铺开讲整个 Ability 生命周期而是用一个最小场景把问题拆清楚连续打开文档详情时同一个文档只保留一个 UIAbility 实例不同文档允许拥有各自独立的实例。本文以 HarmonyOS 7.0 为开发背景。华为当前升级适配指南明确说明HarmonyOS 7.0 对应 API version 26.0.0UIAbility、UIAbilityContext 等 Stage 模型接口本身并不是 HarmonyOS 7 才出现的新能力当前文档继续支持这些接口。一、先看问题为什么连续打开详情页后实例越来越多假设应用有一个文档列表点击“文档 A”打开详情返回列表再次打开“文档 A”再打开“文档 B”接着又打开一次“文档 A”。如果详情只是 ArkUI 页面之间的路由跳转需要关注的是页面栈。但如果每次点击实际调用的是UIAbilityContext.startAbility()那么首先要确认目标 UIAbility 的启动模式。startAbility()是 UIAbilityContext 提供的 UIAbility 启动接口当前 API 参考明确说明该接口属于 Stage 模型能力。这也是一个很容易产生的理解偏差ArkUI 页面实例和 UIAbility 实例不是同一层概念。UIAbility 是 Stage 模型中的应用组件它有自己的onCreate()、onNewWant()、窗口阶段以及前后台生命周期ArkUI 页面则运行在相应 UIAbility 所承载的窗口内容中。所以看到“页面好像重复打开”不要只检查 Navigation 或 Router。只要业务中出现了startAbility()就应该继续检查launchType。二、三种启动模式到底有什么区别Stage 模型针对 UIAbility 提供singleton、multiton和specified三种启动模式。当前官方启动模式文档还特别说明过去使用过的名称standard对应现在的multiton。启动模式通过module.json5中 UIAbility 的launchType配置。AbilityStage 官方文档同时明确指出onAcceptWant()是针对specified启动模式触发的回调。可以先把三种模式压缩成下面这张表启动模式再次启动同类型 UIAbility已存在实例时的关键回调适合的业务singleton复用已有实例onNewWant()首页、主入口等只希望存在一个实例的界面multiton创建新的实例新实例进入onCreate()每次操作都应形成独立实例的场景specified由业务 Key 决定复用还是新建复用时onNewWant()首次创建时onCreate()文档、会话等“同一对象复用不同对象分开”的场景这里真正需要记住的不是三个英文单词而是三个问题系统是直接复用始终新建还是先问业务“这次应该属于哪个实例”三、singleton第二次启动为什么不会重新 onCreate先看最容易理解的singleton。在module.json5中{ module: { // ... abilities: [ { name: DocumentAbility, srcEntry: ./ets/documentability/DocumentAbility.ets, launchType: singleton } ] } }当DocumentAbility还不存在时第一次启动需要创建实例因此会进入onCreate()。如果实例已经存在再次通过startAbility()拉起同一个 UIAbility系统复用已有实例。这一次不会重新创建对象而是把新的 Want 交给onNewWant()。因此下面这样的日志非常有价值import{AbilityConstant,UIAbility,Want}fromkit.AbilityKit;exportdefaultclassDocumentAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{constdocumentIdwant.parameters?.[documentId]asstring;console.info([DocumentAbility] onCreate documentId${documentId});}onNewWant(want:Want,launchParam:AbilityConstant.LaunchParam):void{constdocumentIdwant.parameters?.[documentId]asstring;console.info([DocumentAbility] onNewWant documentId${documentId});}}真正需要关注的是复用实例并不意味着新的启动参数自动变成你的业务状态。例如第一次打开文档 A随后又用新的 Want 打开文档 B。因为是singletonUIAbility 没有重新创建。这时如果业务只在onCreate()中读取documentId后续 Want 就没有被业务处理。所以只要一个 UIAbility 存在被重复拉起、复用实例的可能onNewWant()就值得认真处理。华为当前官方示例也会同时在onCreate()与onNewWant()中读取新传入的 Want 数据。四、multiton为什么越开实例越多把配置换成{ module: { // ... abilities: [ { name: DocumentAbility, srcEntry: ./ets/documentability/DocumentAbility.ets, launchType: multiton } ] } }语义就完全变了。multiton的重点是每次调用启动接口都允许创建新的该类型 UIAbility 实例。因此打开文档 A → DocumentAbility 实例 1 再次打开文档 A → DocumentAbility 实例 2 打开文档 B → DocumentAbility 实例 3如果业务本来只是想刷新一个详情界面却把目标 UIAbility 配成了multiton就很容易出现“为什么越点实例越多”的现象。它并不是错误模式。对于确实需要相互独立的实例multiton正好符合需求。问题在于业务语义是否真的等于“每次启动都是一份新的任务”。如果答案不是就不应该仅仅因为“这个页面可以打开多次”而选择multiton。五、真正适合文档场景的是 specified再来看更有实际价值的一种情况我们希望文档 A → 一个 DocumentAbility 实例 文档 B → 另一个 DocumentAbility 实例 再次打开文档 A → 回到文档 A 已有实例这不是singleton因为 A、B 要允许同时存在。也不是普通的multiton因为重复打开 A 时又不应该继续创建实例。这正是specified要解决的问题。它的核心流程可以理解成startAbility(want) ↓ AbilityStage.onAcceptWant(want) ↓ 生成业务实例标识 ↓ 系统检查该标识对应的 UIAbility 是否已经存在 ↓ 存在 不存在 ↓ ↓ onNewWant() onCreate() 复用原实例 创建新实例AbilityStage 是 module 级别的组件容器。官方文档说明在 HAP 第一次加载时会创建对应的 AbilityStage其中onAcceptWant()会在启动specified类型的 UIAbility 时参与处理。六、搭一个“同一个文档只开一个实例”的最小 Demo假设工程里有EntryAbility ↓ 文档列表 DocumentAbility ↓ 文档内容页列表分别提供三个按钮打开 DOC-1001 打开 DOC-1002 再次打开 DOC-1001我们希望第三次点击时复用第一次创建的实例。1. 配置 DocumentAbilityentry/src/main/module.json5中{ module: { name: entry, type: entry, srcEntry: ./ets/myabilitystage/MyAbilityStage.ets, abilities: [ { name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ets, launchType: singleton }, { name: DocumentAbility, srcEntry: ./ets/documentability/DocumentAbility.ets, launchType: specified, removeMissionAfterTerminate: true } ] } }这里有两个点。一个是DocumentAbility必须配置launchType: specified另一个是 Module 通过srcEntry: ./ets/myabilitystage/MyAbilityStage.ets指定 AbilityStage 的实现文件。AbilityStage 并不是 DevEco Studio 默认模板一定自动生成的内容官方文档给出的开发步骤同样要求按需创建 AbilityStage 文件再通过 Module 的srcEntry指定入口。对于specified模式官方启动模式指导还建议结合业务考虑removeMissionAfterTerminate用于 UIAbility 生命周期结束后移除相应任务避免历史任务影响后续实例匹配。七、从 ArkUI 页面启动 DocumentAbility列表页真正需要做的事情很简单把业务 ID 放进 Want。import{common,Want}fromkit.AbilityKit;import{BusinessError}fromkit.BasicServicesKit;EntryComponentstruct Index{privateopenDocument(documentId:string):void{constcontextthis.getUIContext().getHostContext()ascommon.UIAbilityContext;constwant:Want{bundleName:com.example.abilitydemo,abilityName:DocumentAbility,moduleName:entry,parameters:{documentId:documentId}};context.startAbility(want).catch((err:BusinessError){console.error(startAbility failed, code${err.code}, message${err.message});});}build(){Column({space:16}){Button(打开 DOC-1001).onClick((){this.openDocument(DOC-1001);})Button(打开 DOC-1002).onClick((){this.openDocument(DOC-1002);})Button(再次打开 DOC-1001).onClick((){this.openDocument(DOC-1001);})}.width(100%).padding(24)}}这里不要把documentId只理解成页面参数。对于specified模式它马上还会承担另一项职责参与确定这次启动请求究竟属于哪个 UIAbility 实例。当前 UIAbilityContext API 参考中startAbility(want)的 Promise 形式仍是正式接口系统能力为SystemCapability.Ability.AbilityRuntime.CoreUIAbilityContext 首批 Stage 模型接口从 API version 9 开始提供。本文这个同应用、正常前台启动的最小场景不需要额外声明业务权限。跨应用、后台拉起等情况则受另外的组件启动规则约束不能由这个示例外推。八、onAcceptWant真正决定复用哪个实例的地方接下来创建entry/src/main/ets/myabilitystage/MyAbilityStage.ets代码如下import{AbilityStage,Want}fromkit.AbilityKit;exportdefaultclassMyAbilityStageextendsAbilityStage{onAcceptWant(want:Want):string{if(want.abilityNameDocumentAbility){constdocumentIdwant.parameters?.[documentId];if(typeofdocumentIdstring){constinstanceKeyDocumentAbility_${documentId};console.info([MyAbilityStage] onAcceptWant key${instanceKey});returninstanceKey;}}returndefault;}}这几行代码就是specified最核心的部分。打开DOC-1001返回DocumentAbility_DOC-1001打开DOC-1002返回DocumentAbility_DOC-1002再次打开DOC-1001得到的仍然是DocumentAbility_DOC-1001系统因此可以把第三次请求匹配到已经存在的 DOC-1001 对应实例。这里比较容易理解错onAcceptWant()不是用来“打开页面”的也不是替代startAbility()。它解决的是这次启动请求对应哪个 specified UIAbility 实例所以 Key 一定要来自真正能够代表业务实例身份的数据。文档场景可以用稳定的文档 ID会话窗口可以考虑会话 ID。不要使用每次请求都会变化的随机值否则从实例匹配的角度看每次都是不同对象最终效果就会越来越接近multiton。九、onCreate、onNewWant、onAcceptWant 的日志怎么读给DocumentAbility加上最小日志import{AbilityConstant,UIAbility,Want}fromkit.AbilityKit;exportdefaultclassDocumentAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{constdocumentIdwant.parameters?.[documentId]asstring;console.info([DocumentAbility] onCreate documentId${documentId});}onNewWant(want:Want,launchParam:AbilityConstant.LaunchParam):void{constdocumentIdwant.parameters?.[documentId]asstring;console.info([DocumentAbility] onNewWant documentId${documentId});// 此处根据新的 Want 更新当前实例需要处理的业务状态。}}按照官方定义分析依次启动DOC-1001 DOC-1002 DOC-1001开发者应该重点观察这样的生命周期关系而不是把下面内容当成本文已经真机运行得到的固定日志onAcceptWant → DocumentAbility_DOC-1001 onCreate → DOC-1001 onAcceptWant → DocumentAbility_DOC-1002 onCreate → DOC-1002 onAcceptWant → DocumentAbility_DOC-1001 onNewWant → DOC-1001第三次没有再创建 DOC-1001 对应的新实例因此业务上最关键的信号就是onNewWant()而不是新的onCreate()十、为什么 onNewWant 不能省很多 UIAbility 复用问题最后都会落到这里。例如onCreate(want:Want):void{this.currentDocumentIdwant.parameters?.[documentId]asstring;}如果所有业务初始化都写在onCreate()中那么当实例被系统复用时这部分逻辑不会重新执行。于是开发者可能看到确实回到了原来的窗口但窗口内部展示的数据没有按照新的启动请求更新。正确的思路不是强行把 Ability 改成多实例而是区分两个生命周期语义onCreate → 一个新的 UIAbility 实例被创建 onNewWant → 已经存在的实例收到了新的启动请求如果同一份业务参数在首次创建与实例复用时都需要处理可以进一步抽成一个私有方法privatehandleWant(want:Want):void{constdocumentIdwant.parameters?.[documentId]asstring;console.info(handle documentId${documentId});// 更新业务状态}onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{this.handleWant(want);}onNewWant(want:Want,launchParam:AbilityConstant.LaunchParam):void{this.handleWant(want);}这样至少不会因为生命周期入口不同让首次启动和复用启动走成两套业务逻辑。十一、三种模式怎么选不要只看“能不能多开”实际设计时可以按业务对象之间的关系判断。singleton整个业务只需要一个实例典型语义是无论从哪里进入都回到同一个 UIAbility。例如应用主入口。这种情况下第二次启动时尤其需要关注onNewWant()。multiton每次启动天然就是独立任务语义是这一次启动和前面的实例没有复用关系。如果每次启动都必须形成新的独立实例使用multiton更直接。但不要为了规避状态刷新问题就直接改成multiton。否则原本应该复用的业务对象可能不断增加新的 UIAbility 实例。specified业务 ID 决定实例身份它表达的是同一个业务对象 → 同一个实例 不同业务对象 → 不同实例文档 ID、编辑对象 ID、独立业务会话等都比较接近这个模型。这是三种模式里业务表达能力最强的一种同时也最容易因为 Key 设计错误而失去效果。十二、几个容易理解错的地方1. specified 不是“给 startAbility 多传一个参数”就结束了Want 中的参数只是业务数据。真正把业务数据转换成 UIAbility 实例标识的是AbilityStage.onAcceptWant()如果只传documentId却没有配置 AbilityStage 并实现实例 Key 的返回逻辑就没有完成这个模式最关键的一步。2. onAcceptWant 和 onNewWant 完全不是一回事可以简单分工onAcceptWant → 决定“应该找哪个实例” onNewWant → 已存在实例真正接收这次新的启动参数前者属于 AbilityStage后者属于 UIAbility。AbilityStage 官方开发文档明确把onAcceptWant()描述为 specified UIAbility 启动时使用的事件回调。3. Key 不稳定specified 就失去了复用意义例如returnDocumentAbility_${Date.now()};从业务上看是在打开同一个文档但从系统实例匹配角度看每次 Key 都不同。结果自然会不断产生不同实例。这里应该优先使用稳定、唯一、能够代表该业务对象身份的数据。4. singleton 并不代表业务数据永远不变singleton 保证的是 UIAbility 的实例复用策略不是业务参数固定。新的 Want 仍然可能到来所以需要处理onNewWant()。5. UIAbility 多实例和 ArkUI 页面栈不要混在一起排查如果代码只是Navigation / NavPathStack / 页面路由应先从 ArkUI 导航栈排查。如果出现context.startAbility(...)再继续检查 UIAbility 的启动模式和生命周期。排查层级弄错很容易在页面代码里改很久却没有碰到真正控制实例数量的地方。十三、实际项目中可以按这个顺序排查遇到“Ability 为什么越开越多”“为什么第二次打开没有重新初始化”这类问题我更建议按下面的顺序看代码。第一步看调用方式。确认究竟是 ArkUI 页面导航还是调用了startAbility()第二步看module.json5。找到目标 UIAbility 的launchType确认到底是singleton multiton specified第三步看生命周期日志。至少把下面几个位置打出来AbilityStage.onAcceptWant UIAbility.onCreate UIAbility.onNewWant这样很快就能判断系统到底是在创建还是复用。第四步如果是 specified直接打印实例 Key。检查同一业务对象是否真的返回相同 KeyDOC-1001 → DocumentAbility_DOC-1001 DOC-1001 → DocumentAbility_DOC-1001而不是DOC-1001 → key1 DOC-1001 → key2第五步检查 onNewWant 的业务处理。实例复用了不等于界面状态自动刷新。真正需要更新的业务参数仍然应该由应用处理。第六步再检查任务和生命周期配置。特别是 specified 场景还要结合任务移除策略、UIAbility 是否已经被销毁等情况判断。已经销毁的实例当然不存在“继续复用”。开发经验总结UIAbility 启动模式看起来只有一个launchType字段但它实际决定的是实例管理方式。singleton适合“这一类 UIAbility 只保留一个”的场景。已经存在的实例被再次启动时需要重点处理onNewWant()。multiton表达的是“每次启动都可以拥有独立实例”。如果原本希望重复打开同一业务对象仍然复用就不要用它来回避状态管理问题。specified更适合文档这种“按业务对象管理实例”的模型。它的关键不只在launchType: specified而是这一整条链Want 携带业务 ID ↓ AbilityStage.onAcceptWant() ↓ 生成稳定实例 Key ↓ 系统匹配实例 ↓ 新实例 → onCreate() 已有实例 → onNewWant()真正排查“UIAbility 为什么越开越多”时也建议沿着这条链去找而不是看到多个界面就直接把问题归到 ArkUI 页面层。如果你的应用里也存在“同一文档、同一聊天对象、同一业务任务只能拥有一个独立窗口”的要求可以重点检查现在用于实例识别的 Key 是否稳定以及复用后有没有完整处理onNewWant()。这两个地方往往比startAbility()本身更值得关注。 写在最后如果你觉得这篇文章对你有帮助或者有任何想法、建议欢迎在评论区留言交流你的每一个点赞 、收藏 ⭐、关注 ❤️都是我持续更新的最大动力我是一个在代码世界里不断摸索的小码农愿我们都能在成长的路上越走越远越学越强感谢你的阅读我们下篇文章再见✍️ 作者菜鸟不学编程 本文原创转载请注明出处。