Harmony os——EmbeddedUIExtensionAbility 笔记:跨进程界面嵌入怎么用?

67 阅读4分钟

EmbeddedUIExtensionAbility 笔记:跨进程界面嵌入怎么用?

这一篇是我给 EmbeddedUIExtensionAbility 写的学习笔记。 关键词就是:跨进程 UI 嵌入 + 模块隔离 + 和 EmbeddedComponent 配合使用


1. 它到底是干嘛的?

官方一句话:

EmbeddedUIExtensionAbility 是 EMBEDDED_UI 类型的 ExtensionAbility,提供跨进程界面嵌入能力。

翻译成「人话」就是:

  • 有一个 UIAbility 在主进程 跑着;
  • 它的页面里可以放多个 EmbeddedComponent
  • 每个 EmbeddedComponent 都可以把 另一个进程里 的 UI(就是 EmbeddedUIExtensionAbility 提供的)嵌进来;
  • 嵌进来的那块 UI:布局、渲染在单独进程完成,和 UIAbility 的数据不直接互通

非常适合这种场景:

  • 你做一个「办公类应用」:

    • 主 UIAbility:负责整体 Shell(菜单栏、侧边栏、容器布局);
    • 文档编辑、表格编辑、PPT 编辑: 各自用一个 EmbeddedUIExtensionAbility,在独立进程里跑;
  • 这样:

    • 各模块互相隔离(一个崩了不一定拖垮主进程);
    • 更符合「大应用拆解成多个可独立演进模块」的思路。

2. 使用前先看限制

有两个很重要的前提:

  1. 只支持有「多进程配置」的设备

    • 目前文档里点名支持:2in1Tablet
    • 也就是说在一些手机设备上,暂时是用不了这套的。
  2. 一些强约束:

    • EmbeddedComponent 只能在 UIAbility 中使用
    • 被拉起的 EmbeddedUIExtensionAbility 必须和 UIAbility 属于同一个应用(同一个 Bundle)

3. 生命周期:跟 UIExtensionAbility 一脉相承

EmbeddedUIExtensionAbility 的生命周期回调继承自 UIExtensionAbility,主要有这些:

  • onCreate() 创建 ExtensionAbility 实例时调用,一般做一次性的初始化。

  • onSessionCreate(want, session) 界面内容对象创建后 调用:

    • 会拿到一个 UIExtensionContentSession
    • 通常在这里 session.loadContent() 把具体 UI 页面加载出来。
  • onSessionDestroy(session) 界面内容对象销毁时调用,可以做会话级别的资源清理。

  • onForeground() 从后台切到前台时调用。

  • onBackground() 从前台切到后台时调用。

  • onDestroy() EmbeddedUIExtensionAbility 实例销毁时调用,做整体资源清理。

可以简单把它看成:

一个「跑在独立进程里的小 UI 模块」,通过 Session 跟宿主产生一对一的嵌入关系。


4. 提供方开发:写一个 EmbeddedUIExtensionAbility

4.1 新建能力文件

在 DevEco Studio 里大致步骤:

  1. 在某个 Module 的 ets 目录下,新建目录,比如:EmbeddedUIExtAbility/
  2. 在这个目录里新建 EmbeddedUIExtAbility.ets 文件;
  3. 在里面继承 EmbeddedUIExtensionAbility,实现相关生命周期。

示例代码(提供方):

 import { EmbeddedUIExtensionAbility, UIExtensionContentSession, Want } from '@kit.AbilityKit';
 ​
 const TAG: string = '[ExampleEmbeddedAbility]';
 ​
 export default class ExampleEmbeddedAbility extends EmbeddedUIExtensionAbility {
   onCreate() {
     console.info(TAG, `onCreate`);
   }
 ​
   onForeground() {
     console.info(TAG, `onForeground`);
   }
 ​
   onBackground() {
     console.info(TAG, `onBackground`);
   }
 ​
   onDestroy() {
     console.info(TAG, `onDestroy`);
   }
 ​
   onSessionCreate(want: Want, session: UIExtensionContentSession) {
     console.info(TAG, `onSessionCreate, want: ${JSON.stringify(want)}`);
 ​
     // 把 session 丢进 LocalStorage,给页面用
     let param: Record<string, UIExtensionContentSession> = {
       'session': session
     };
     let storage: LocalStorage = new LocalStorage(param);
 ​
     // 加载真正显示 UI 的页面
     session.loadContent('pages/extension', storage);
   }
 ​
   onSessionDestroy(session: UIExtensionContentSession) {
     console.info(TAG, `onSessionDestroy`);
   }
 }

关键点:

  • onSessionCreate() 里通过 session.loadContent('pages/extension', storage) 加载 UI;
  • 通过 LocalStoragesession 传到页面端,方便在页面逻辑中访问。

4.2 扩展页面:真正渲染的 UI

pages/extension.ets 大概这样写:

 import { UIExtensionContentSession } from '@kit.AbilityKit';
 ​
 @Entry()
 @Component
 struct Extension {
   @State message: string = 'EmbeddedUIExtensionAbility Index';
 ​
   // 拿到 SharedLocalStorage
   localStorage: LocalStorage | undefined = this.getUIContext().getSharedLocalStorage();
   private session: UIExtensionContentSession | undefined =
     this.localStorage?.get<UIExtensionContentSession>('session');
 ​
   build() {
     Column() {
       Text(this.message)
         .fontSize(20)
         .fontWeight(FontWeight.Bold)
 ​
       Button('terminateSelfWithResult')
         .fontSize(20)
         .onClick(() => {
           this.session?.terminateSelfWithResult({
             resultCode: 1,
             want: {
               bundleName: 'com.example.embeddeddemo',
               abilityName: 'ExampleEmbeddedAbility'
             }
           });
         })
     }
     .width('100%')
     .height('100%')
   }
 }

这里有几个知识点:

  • 通过 getSharedLocalStorage() 拿到刚刚在扩展里塞进去的 LocalStorage;

  • 再用 key 'session' 取出 UIExtensionContentSession

  • session.terminateSelfWithResult(...)

    • 表示当前嵌入的这个 UI 模块「带着结果主动结束」;
    • 外层的 EmbeddedComponent 可以通过回调拿到 resultCodewant

4.3 在 module.json5 里注册

最后,得在 module.json5 中把这个 ExtensionAbility 注册出来:

 {
   "module": {
     "extensionAbilities": [
       {
         "name": "EmbeddedUIExtAbility",
         "icon": "$media:icon",
         "description": "EmbeddedUIExtAbility",
         "type": "embeddedUI",      // ★ 类型必须是 embeddedUI
         "srcEntry": "./ets/EmbeddedUIExtAbility/EmbeddedUIExtAbility.ets"
       }
     ]
   }
 }

这里最重要的两个字段:

  • "type": "embeddedUI":指明这是 EmbeddedUIExtensionAbility;
  • "srcEntry":指向刚才写的 .ets 文件。

5. 使用方开发:UIAbility 页面里嵌入它

提供方搞定之后,就轮到使用方 —— 也就是 UIAbility 页面,这里主要用 EmbeddedComponent

示例:pages/Index.ets

 import { Want } from '@kit.AbilityKit';
 import { BusinessError } from '@kit.BasicServicesKit';
 ​
 @Entry
 @Component
 struct Index {
   @State message: string = 'Message: '
 ​
   private want: Want = {
     bundleName: 'com.example.embeddeddemo',
     abilityName: 'EmbeddedUIExtAbility',
     parameters: {
       // 控制进程模式的参数(后面解释)
       'ohos.extension.processMode.hostInstance': 'true'
     }
   }
 ​
   build() {
     Row() {
       Column() {
         Text(this.message).fontSize(30)
 ​
         EmbeddedComponent(this.want, EmbeddedType.EMBEDDED_UI_EXTENSION)
           .width('100%')
           .height('90%')
           .onTerminated((info: TerminationInfo) => {
             this.message = 'Termination: code = ' + info.code
               + ', want = ' + JSON.stringify(info.want);
           })
           .onError((error: BusinessError) => {
             this.message = 'Error: code = ' + error.code;
           })
       }
       .width('100%')
     }
     .height('100%')
   }
 }

你可以重点记住:

  • EmbeddedComponent(this.want, EmbeddedType.EMBEDDED_UI_EXTENSION)

    • want 用来指定「嵌入哪一个 EmbeddedUIExtensionAbility」;
    • 第二个参数使用 EMBEDDED_UI_EXTENSION 类型;
  • onTerminated:对方调用 terminateSelfWithResult 时,这里会回调;

  • onError:嵌入过程出错时的兜底处理。


6. 进程模式:两个关键参数怎么玩?

EmbeddedUIExtensionAbility 支持更灵活的进程控制,通过 want.parameters 里的两个字段来调:

  1. ohos.extension.processMode.hostSpecified

    • 含义:指定 非首次启动时 的进程名;

    • 例子:

       "ohos.extension.processMode.hostSpecified": "com.ohos.inentexecutedemo:share"
      
    • 作用:告诉系统「之后再启动这个 EmbeddedUIExtensionAbility,都跑在这个进程里」。

  2. ohos.extension.processMode.hostInstance

    • 含义:控制是否按「独立进程」启动;

    • 入参是字符串 "true" / "false"(注意是字符串):

      • "false":跟随 UIExtensionAbility 默认的进程模型;
      • "true"无论 Extension 配置了什么进程模型,都会新建一个独立进程。
  3. 冲突时谁优先?

    如果两个字段都配了,hostSpecified 优先级更高,也就是:

    hostSpecified 就按它的进程名跑,忽略 hostInstance

如何选用?

  • 需要「强隔离、每个嵌入实例一个独立进程」:

    • 'ohos.extension.processMode.hostInstance': 'true'
  • 希望「多个嵌入共用一个特定进程」:

    • 'ohos.extension.processMode.hostSpecified': 'xxx:your_process'
  • 如果两个场景都想用:

    • 可以按业务类别拆分多个 EmbeddedUIExtensionAbility,分别配置不同的进程策略。

7. 我的实践向理解小结

我会把这套能力理解成:

“把一个复杂 UI 模块塞进独立进程,然后再嵌进主 UIAbility 里使用”

  • 适合:

    • 大型、复杂、容易出问题的模块(例如渲染引擎、三方文档渲染、编辑器内核);
    • 对稳定性要求很高,希望「某个子模块挂掉不拖死整个 App」的场景;
  • 不太适合:

    • 只做简单展示的小组件(进程开销会显得比较重);
    • 对性能极度敏感、切换频繁且体量很小的 UI(频繁起停进程会较重)。