Harmony os——UIAbility 组件启动模式彻底搞懂(singleton / multiton / specified)

64 阅读6分钟

Harmony os——UIAbility 组件启动模式彻底搞懂(singleton / multiton / specified)

这篇是我结合官方文档,按自己的理解重新整理的一篇笔记,方便以后写 HarmonyOS 博客用。 主题只有一个:UIAbility 有哪几种启动模式,各自适合什么业务场景,怎么配置,生命周期有什么区别。


1. 为什么要关心“启动模式”?

在 Stage 模型里,UIAbility 是系统调度的基本单元,对应任务视图里的一个「任务卡片」。 同一个 UIAbility:

  • 可以只存在 一个实例(比如“首页”)
  • 也可以存在 多个实例(比如“一个文档开多份”、“多个聊天窗口”)
  • 甚至可以按 某个业务 Key 精准匹配到某个实例(比如“同一个文档不重复创建新窗口”)

这些行为,靠的就是 launchType(启动模式)

  • singleton:单实例模式(默认)
  • multiton:多实例模式(以前叫 standard
  • specified:指定实例模式(带 Key 匹配)

2. 三种启动模式总览

先给一个“脑图式”总结:

  • singleton(单实例)

    • 同一个 UIAbility,进程里始终只有一个实例
    • 再次 startAbility() → 不新建实例,只走 onNewWant()
    • 最近任务列表里只看到 一个任务卡片
  • multiton(多实例)

    • 每次 startAbility()新建一个实例
    • 可以开多个同类型 UIAbility(类似“多个窗口”)
    • 最近任务列表里能看到多个同名任务
  • specified(指定实例)

    • 启动时通过 Want.parameters 传一个 instanceKey
    • 系统先走 AbilityStage.onAcceptWant() 获取 实例标识
    • 如果匹配到已存在实例 → 唤醒并 onNewWant()
    • 如果没匹配到 → 创建新实例(走 onCreate() + onWindowStageCreate()

3. singleton:单实例模式(默认)

3.1 行为特点

  • 一种 UIAbility 类型,在一个应用进程中 只会有一个实例

  • 每次调用 startAbility()

    • 如果该 UIAbility 还没创建 → 正常创建,走 onCreate() → onWindowStageCreate() → onForeground()
    • 如果该 UIAbility 已经存在 → 只会触发 onNewWant()不会再创建新实例
  • 最近任务列表里,这个 UIAbility 只出现一张卡片

官方说明点:

  • 如果已有实例正在启动过程中,此时再次 startAbility() 启动同一个单实例 UIAbility,会直接报错,错误码 16000082

3.2 配置方式

module.json5 里配置 launchType

 {
   "module": {
     // ...
     "abilities": [
       {
         "name": "EntryAbility",
         "launchType": "singleton",
         // ...
       }
     ]
   }
 }

不写 launchType 的话,默认就是 singleton

3.3 适用场景

比较典型的:

  • App 主页 / Tab 容器:只需要一个实例,重复打开就复用。
  • 设置页 / 个人中心:不需要开多份,只要一个入口。
  • 入口 UIAbility:希望在任务视图中只显示一个任务。

4. multiton:多实例模式

4.1 行为特点

  • 每次 startAbility() 都会创建一个新的 UIAbility 实例
  • 生命周期每次都是完整跑一遍:onCreate() → onWindowStageCreate() → onForeground() ...
  • 最近任务列表里,可以看到多个同类型 UIAbility 的任务卡片。
  • standard 是以前的叫法,现在统一叫 multiton,效果一样。

4.2 配置方式

 {
   "module": {
     // ...
     "abilities": [
       {
         "name": "ChatAbility",
         "launchType": "multiton",
         // ...
       }
     ]
   }
 }

4.3 适用场景

适合那种“天然多窗口”的业务:

  • 聊天类:多个会话窗口 各开一份 UIAbility。
  • 编辑类:多个草稿 / 草图 /任务 并行编辑。
  • 工具类:需要同时打开多个独立任务界面。

缺点也很明显:实例越多,内存越吃紧,需要控制数量和回收。


5. specified:指定实例模式(带 Key 匹配)

这个模式是三种里最“聪明”的一种,适合类似“文档 / 项目”这类有明确唯一标识的业务。

5.1 核心思想

给每个 UIAbility 实例绑定一个 Key,这个 Key 由业务自己定义,比如:

  • 文档路径
  • 项目 ID
  • 会话 ID

启动时流程大致是这样:

  1. 调用方(比如 EntryAbility)用 startAbility() 启动 SpecifiedAbilityWant.parameters 中放一个 Key:instanceKey

  2. 系统在真正拉起目标 UIAbility 前,先调用它所在 HAP 上的 AbilityStage.onAcceptWant()

    • 由开发者根据 want 返回一个实例标识字符串
  3. 系统根据这个标识去查:

    • 如果已经有一个实例的标识一样 → 唤醒那个实例,走 onNewWant()
    • 如果没有匹配到 → 新建一个实例,走 onCreate() + onWindowStageCreate()

可以理解为: specified = multiton + “按 Key 重用实例”

5.2 配置:先把 UIAbility 声明成 specified

在被指定实例模式使用的 UIAbility 上,先配置:

 {
   "module": {
     // ...
     "abilities": [
       {
         "name": "SpecifiedFirstAbility",
         "launchType": "specified",
         // ...
       }
     ]
   }
 }

5.3 调用方:在 Want 里传入 instanceKey

EntryAbility 或某个页面里,通过 startAbility() 启动它,同时传入 instanceKey

 import { common, Want } from '@kit.AbilityKit';
 import { hilog } from '@kit.PerformanceAnalysisKit';
 import { BusinessError } from '@kit.BasicServicesKit';
 ​
 const TAG: string = '[Page_StartModel]';
 const DOMAIN_NUMBER: number = 0xFF00;
 ​
 function getInstance(): string {
   return 'KEY';
 }
 ​
 @Entry
 @Component
 struct Page_StartModel {
   private KEY_NEW = 'KEY';
 ​
   build() {
     Row() {
       Column() {
         // 启动 SpecifiedFirstAbility,Key 递增
         Button('Open First')
           .onClick(() => {
             const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
             const want: Want = {
               deviceId: '',
               bundleName: 'com.samples.stagemodelabilitydevelop',
               abilityName: 'SpecifiedFirstAbility',
               moduleName: 'entry',
               parameters: {
                 instanceKey: this.KEY_NEW
               }
             };
             context.startAbility(want).then(() => {
               hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in starting SpecifiedAbility.');
             }).catch((err: BusinessError) => {
               hilog.error(DOMAIN_NUMBER, TAG,
                 `Failed to start SpecifiedAbility. Code is ${err.code}, message is ${err.message}`);
             });
             this.KEY_NEW = this.KEY_NEW + 'a';
           })
 ​
         // 启动 SpecifiedSecondAbility,Key 固定为 KEY
         Button('Open Second')
           .onClick(() => {
             const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
             const want: Want = {
               deviceId: '',
               bundleName: 'com.samples.stagemodelabilitydevelop',
               abilityName: 'SpecifiedSecondAbility',
               moduleName: 'entry',
               parameters: {
                 instanceKey: getInstance()
               }
             };
             context.startAbility(want).then(() => {
               hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in starting SpecifiedAbility.');
             }).catch((err: BusinessError) => {
               hilog.error(DOMAIN_NUMBER, TAG,
                 `Failed to start SpecifiedAbility. Code is ${err.code}, message is ${err.message}`);
             });
           })
       }
       .width('100%')
     }
     .height('100%')
   }
 }

可以类比:

  • 每个 instanceKey = 一个“文档 / 会话”的 ID
  • 以后只要带着相同的 Key 启动,就会跑回同一个实例。

5.4 被调用方:在 AbilityStage 中根据 Want 返回“实例标识”

接着,在这个模块对应的 AbilityStage 中,通过 onAcceptWant()Want 转成一个唯一字符串标识:

 import { AbilityStage, Want } from '@kit.AbilityKit';
 ​
 export default class MyAbilityStage extends AbilityStage {
   onAcceptWant(want: Want): string {
     // 针对 launchType 为 specified 的 UIAbility,返回实例 Key
     if (want.abilityName === 'SpecifiedFirstAbility' ||
         want.abilityName === 'SpecifiedSecondAbility') {
       if (want.parameters) {
         // 比如:SpecifiedAbilityInstance_某个Key
         return `SpecifiedAbilityInstance_${want.parameters.instanceKey}`;
       }
     }
     // 其他情况的默认标识
     return 'MyAbilityStage';
   }
 }

关键点:

  • onAcceptWant() 返回的字符串决定了“这个 want 要归属到哪个实例”;
  • 系统内部会用这个字符串当作「UIAbility 实例的身份标识」;
  • 标识一样 → 认为是同一个实例 → 只走 onNewWant()
  • 标识不同 → 创建新实例。

⚠ DevEco Studio 默认工程不会自动生成 AbilityStage 文件,需要自己按步骤新建。


5.5 一个典型场景:文档应用

拿文档编辑 App 举个“完整”例子,会更好理解:

  • 打开文件 A → 创建 UIAbility 实例 1,Key = doc:/A

  • 在最近任务中滑掉 A → 实例 1 被销毁

  • 回到桌面再打开 A → 创建 UIAbility 实例 2,Key 仍为 doc:/A

  • 打开文件 B → 创建 UIAbility 实例 3,Key = doc:/B

  • 回到桌面,再次打开 A

    • 系统根据 instanceKey = doc:/A 调用 onAcceptWant()
    • 匹配到之前的 UIAbility 实例 2
    • 于是直接把实例 2 拉到前台(走 onNewWant()),而不是新建实例

这个模式刚好解决了:

  • “新建文档要开新窗口”
  • “再打开旧文档时,回到原来的编辑界面”的需求 → 非常适合编辑器 / IDE / 文档类应用。

6. 生命周期和启动模式的关系补充一下

  • singleton / specified(匹配已有实例)

    • 再次 startAbility() 时:

      • 不会再走 onCreate()onWindowStageCreate()
      • 只会触发 onNewWant()
  • multiton / specified(没有匹配到实例)

    • 每一次启动都是:

      • onCreate()
      • onWindowStageCreate()
      • onForeground()

7. 我自己的使用习惯小总结

如果我来设计一个 App,常规会这么用:

  • 主页 / Tab 容器singleton

  • 单独的一个功能窗口(设置、关于、搜索结果) :多数也是 singleton

  • 可同时打开多个任务的模块(文档 / 会话 / 项目)

    • 如果不需要区分“旧文档 / 新文档”,只要多窗口 → multiton
    • 需要“按文档 ID 重用窗口” → specified + instanceKey
  • 涉及“按 Key 复用实例”的,一律考虑 specified,配合 AbilityStage.onAcceptWant() 做精细控制