FlexUI OHOS 多运行时设计:VM/Scope 显式寻址与多 Scope 隔离

日期: 2026-08-05 | 分支: ai | 平台: OHOS (HarmonyOS) 关联: docs/design/flexui-ohos-runtime-isolation.md(现状梳理)、docs/bugfix/2026-08-05-multicard-scenebuilder-root-routing.md 状态: 设计定稿,按此实施

一、背景与目标

现状(见 flexui-ohos-runtime-isolation.md):OHOS 上 JS 运行时被 TS 封装层收敛为进程级单例 —— 1 个 JSVM(groupId=1000)、1 个 Scope/Context(globalThis)、所有卡片共享。隔离仅停留在 instance 逻辑层 + rootId 渲染层,任一张卡片的全局污染会影响全部。

需求(用户决策):

  1. 移除单例设计,统一显式创建:删除 ensureInit/sInstance/instance,createEngine 为唯一创建入口;engineKey/groupId/scopeKey 全部必选,引擎内部不做默认值兜底,由调用方自行维护默认 key/id 常量。
  2. 多 Scope 是本次隔离的主要手段:同一 Engine(同一 JSVM,共享堆)内创建多个 Scope(各自独立 Context/globalThis),卡片按 scope 路由,实现环境级(context 级)隔离,满足当前隔离要求。

设计原则:

  • 显式优于隐式:createEngine 唯一创建入口,engineKey/groupId/scopeKey 必选、无默认值兜底(调用方维护默认常量);单 scope 路径的 key 与多 scope 完全一致。
  • 最小侵入:C++ 层已天然支持多 VM(reuse_engine_map 按 groupId)/多 Scope(每次 JsDriver_CreateJsDriver 新建 scope+context),不修改 C++;全部改动集中在 TS 层。
  • 两级寻址:engineKey → Engine(VM 池),scopeKey → Engine 内 Scope(Context/globalThis)。

二、现状关键代码事实(改动依据)

层 事实 位置
C++ VM 复用 reuse_engine_map<groupId, (Engine*, refcount)>;-1 新建、其他值共享、归零销毁 driver/js/src/js_driver_utils.cc:117,189,593
C++ Scope 每次 JsDriver_CreateJsDriver → CreateScopeAndAsyncInitialize → engine->CreateScope("") + scope->CreateContext(),scope_id 全局自增 framework/ohos/src/main/cpp/impl/connector/src/js_driver_napi.cc:140-214、js_driver_utils.cc:351
TS Engine createFlexUIEngine(params) 每次 new FlexUIEngineManagerImpl;FlexUIGlobalConfigs/mEngineContext/JsDriver/BridgeManager 全 per-instance;非单例 flexui_framework/FlexUIEngine.ets:83-106、FlexUIEngineManagerImpl.ets:97-130
TS Context 每 context 独立 JsDriver + FlexUIBridgeManagerImpl + FlexUIModuleManagerImpl + NativeRenderProvider + DomManager FlexUIEngineContextImpl.ets:136-185
单例收敛点 1 FlexUIEngine.ensureInit / sInstance / instance flexui/FlexUIEngine.ets:143-173
单例收敛点 2 FLEXUI_GROUP_ID = 1000 硬编码到 params.groupId flexui/FlexUIEngine.ets:31,328
单例收敛点 3 FlexView.aboutToAppear 硬取 FlexUIEngine.instance flexui/FlexView.ets:140-153
单槽位字段 FlexUIEngineManagerImpl.mEngineContext(1 个 context)、rootView/moduleLoadParams/mCoreBundleLoader/mModuleListener("当前处理"语义) FlexUIEngineManagerImpl.ets:81,87-91
全局静态点 EnvAPI._bridgeManager 模块级静态,_setBridgeManager 单值注入 flexui_framework/modules/javascript/EnvAPI.ets:28,60
渲染回调 onFirstPaint/onFirstContentfulPaint 无 rootId 参数,经 per-scope frameworkProxy 回到 manager 单槽位 mModuleListener renderer_native/FrameworkProxy.ets:22-24、NativeRenderImpl.ets:152-162

三、总体架构

FlexUIEngine (facade, flexui/)
 ├─ sRegistry: Map<engineKey, FlexUIEngine>      ← 新增:Engine 注册表
 │   └─ FlexUIEngineManagerImpl (native engine)
 │       ├─ mScopes: Map<scopeKey, FlexUIEngineContextImpl>   ← 新增:Scope 注册表
 │       │   └─ 主 Scope(key = config.scopeKey,engine 初始化即创建)
 │       │       └─ FlexUIEngineContextImpl
 │       │           ├─ JsDriver (instanceId = C++ scope_id)
 │       │           ├─ FlexUIBridgeManagerImpl (bridge state per-scope)
 │       │           ├─ FlexUIModuleManagerImpl (native modules per-scope)
 │       │           ├─ NativeRenderProvider / NativeRenderer
 │       │           └─ DomManager
 │       │   └─ 额外 scope → createScope(scopeKey) 追加
 │       └─ cardSessions: Map<rootId, CardSession>   ← 现有,扩展 scopeKey/loader 字段
 │
 └─ FlexView (组件)
     ├─ engineKey: string (必填 @Require)   ← 关联 Engine
     └─ scopeKey: string (必填 @Require)    ← 关联 Scope

隔离效果对照:

维度 现状(单 scope) 多 scope(同 engine) 多 engine(不同 groupId)
JSVM/堆 共享 1 个 共享 1 个(同 groupId) 隔离
Context/globalThis 共享 1 个 每 scope 独立 独立
卡片 JS 状态 instance 逻辑隔离 instance + 环境隔离 全隔离
jsfwk 成本 1 份 每 scope 1 份 每 engine 1 份

四、API 设计(TS 层)

4.1 常量与配置(flexui/FlexUIEngine.ets)

// 引擎不导出默认 key 常量(不兜底)。调用方自行维护(示例):
const ENGINE_KEY = 'default'      // 引擎注册表 key
const SCOPE_KEY = 'default'       // 主 scope key
const ENGINE_GROUP_ID = 1000      // JSVM groupId

export interface FlexUIEngineConfig {
  // ...现有字段不变
  /** Engine 注册表 key(必填,由调用方维护默认 key)。FlexView 通过 engineKey 关联。 */
  engineKey: string;
  /** 主 scope key(必填):引擎启动即创建该 scope(独立 Context/globalThis)。 */
  scopeKey: string;
  /** JSVM groupId(必填,由调用方维护):-1 每次新建 VM;其他值同组共享。 */
  groupId: number;
}

4.2 facade 注册表(flexui/FlexUIEngine.ets)

export class FlexUIEngine {
  /** 创建 Engine 并注册(唯一入口,key = config.engineKey,必填)。幂等:同 key 已存在返回已有实例。 */
  static createEngine(config: FlexUIEngineConfig): FlexUIEngine

  /** 按 key 取 Engine(key 必填,由调用方维护)。 */
  static get(key: string): FlexUIEngine | null

  /** 销毁并注销指定 Engine(key 必填)。 */
  static destroy(key: string): void
  static destroyAll(): void

  /** 实例 API */
  get engineKey(): string
  /** 在 Engine 内创建 Scope(独立 globalThis)。幂等。 */
  createScope(scopeKey: string, onScopeReady?: () => void): void
  destroyScope(scopeKey: string): void
  /** scope 就绪回调(scopeKey 必填,由调用方维护,如 'default' 主 scope) */
  onReady(callback: () => void, scopeKey: string): void
  /** loadCard 透传 scopeKey 路由(必填) */
  loadCard(url, buffer, componentName, params, modules, client, scopeKey: string): FlexUIRootView | null
  /** 取指定 scope 的 context(scopeKey 必填,用于渲染绑定等) */
  getScopeContext(scopeKey: string): FlexUIEngineContext | null
}

说明:单例时代设计(ensureInit / sInstance / instance getter)已按需求移除,引擎不再有默认 key/id 兜底;调用方自行维护默认常量(如 ENGINE_KEY='default'、ENGINE_GROUP_ID=1000)并显式传入。

4.3 native Engine 接口扩展(flexui_framework/FlexUIEngine.ets 接口 + Impl)

FlexUIEngine 接口新增:

createScope(scopeKey: string, scopeInitCallback: (result: number, reason: string) => void): boolean
destroyScope(scopeKey: string): void
loadModuleWithListener(loadParams, listener, scopeKey: string): FlexUIRootView | null
getScopeContext(scopeKey: string): FlexUIEngineContext | null
getScopeNativeRenderProvider(scopeKey: string): NativeRenderProvider | null

五、实现细节

5.1 FlexUIEngineManagerImpl:单 context → 多 scope(核心)

存储:private mScopes: Map<string, FlexUIEngineContextImpl> = new Map(); getFlexUIEngineContext() 返回主 scope(key = params.scopeKey,由调用方 createEngine 指定,不存在时返回 null)。

startEngine(主 scope 创建,现状路径):抽 createScopeInternal(scopeKey, domMgr, coreBundleLoader, onReLoad, initCallback):

private createScopeInternal(scopeKey, domMgr, coreBundleLoader, onReLoad, initCallback): FlexUIEngineContextImpl | null {
  const ctx = new FlexUIEngineContextImpl(this.params, domMgr, this.mGlobalConfigs,
    this.mInitStartTime, this.mMonitor, coreBundleLoader, this.mDevSupportManager, this.reloadEngineCallback)
  this.mScopes.set(scopeKey, ctx)
  if (onReLoad && this.mDebugMode && this.rootView) { ctx.setRootView(this.rootView) }
  const provider = ctx.getNativeRenderProvider()
  provider?.getNativeRenderImpl().setFrameworkProxy(this)
  if (!this.params.integrateJSEngine) { initCallback(EngineInitStatus.STATUS_OK, ''); return ctx }
  ctx.getBridgeManager()?.initBridge((result: number, reason: string) => {
    ctx.nativeRenderer?.initRendererParams(px2vp(DimensionsUtil.STATUS_BAR_HEIGHT))
    initCallback(result, reason)
  }, onReLoad)
  return ctx
}

startEngine 内部改为 createScopeInternal(this.params.scopeKey, domMgr, this.mCoreBundleLoader, onReLoad, cb),cb 内 _setBridgeManager(bridge, this.params.scopeKey) + 原 initBridgeCallback 逻辑(engine 级 ready)。

createScope(scopeKey, scopeInitCallback):已存在则告警返回 false;否则 createScopeInternal(scopeKey, null, this.mCoreBundleLoader, false, cb),cb 内 _setBridgeManager(ctx.getBridgeManager(), scopeKey) + scopeInitCallback(result, reason)。新 scope 的 core bundle(flexui_jsfwk.js)由 initBridge 内部加载(FlexUIBridgeManagerImpl.initJSBridgeCallback),apiJSAssetsPaths 等由 facade 层在 scopeInitCallback 后加载(见 5.2)。

destroyScope(scopeKey):取 ctx → destroyBridge(false) → destroy(false)(释放 DomManager/render/module/vfs)→ 从 mScopes 移除 → _setBridgeManager(null, scopeKey)。主 scope 不允许直接销毁(走 destroyEngine)。

destroyEngine():遍历 mScopes 逐个 destroyBridge(false),清空 Map,_setBridgeManager(null, key) 逐 key。

5.2 facade:scope bundles 加载与 ready 语义

现状 doInit 的 onInitialized 里收集 apiJSAssetsPaths + extensions jsBundlePath,经主 scope bridge 链式加载后 fireReady。抽为:

private loadScopeBundles(bridgeMgr: FlexUIBridgeManager | null, onDone: () => void): void
  • 主 scope 初始化后调用(现状行为不变,加载完 fireReady);
  • createScope(scopeKey, onScopeReady) 的 scopeInitCallback 里调用(新 scope 的 core bundle 已由 initBridge 加载完),onDone = 标记 scope ready + fire scope 级 listeners。

ready 状态:facade 维护 mScopeReady: Map<string, boolean>、mScopeReadyListeners: Map<string, Array<() => void>>。主 scope ready = 原 isReady(mScopeReady['default'] 与 isReady 同步)。

onReady(callback, scopeKey?):scopeKey 缺省 = 'default'(原语义);已 ready 立即执行,否则入队。

5.3 卡片路由:loadModuleWithListener + loadJsModule per-scope

CardSession 扩展(FlexUIEngineManagerImpl.ets:51-55):

interface CardSession {
  rootView: FlexUIRootView;
  loadParams: ModuleLoadParams;
  moduleListener: ModuleListener | null;
  coreBundleLoader: FlexUIBundleLoader | null;   // 新增:per-card loader(原 mCoreBundleLoader)
  scopeKey: string;                               // 新增
}

loadModuleWithListener(loadParams, listener, scopeKey?):

  1. const ctx = scopeKey ? this.mScopes.get(scopeKey) : (主 scope);不存在 → 报错返回 null(提示先 createScope);
  2. 卡片 loader 创建逻辑不变(per-card FlexUIAssetBundleLoader),codeCacheTag 派生:loadParams.codeCacheTag 非空时追加 '_' + scopeKey,避免多 scope 写同一字节码缓存文件;
  3. ctx.createRootView(loadParams)(注意:现状用 this.mEngineContext!,改为目标 ctx);
  4. session 存 cardSessions,同时更新 mLastSession(兼容 debug reload 路径);
  5. ctx.setRootView/ setLoadModuleListener/ setComponentName(per-scope 槽位,天然隔离);
  6. loadJsModule(session)。

loadJsModule(sessionOverride?):参数化,不再读单槽位字段:

  • const session = sessionOverride ?? this.mLastSession;const ctx = this.mScopes.get(session.scopeKey);
  • mCoreBundleLoader → session.coreBundleLoader;rootView/moduleLoadParams → session.rootView/session.loadParams;
  • 错误与完成回调走 notifyModuleLoaded(status, msg, session)。

notifyModuleLoaded(status, msg, session?):(session ?? mLastSession)?.moduleListener?.onLoadCompleted(...);mModuleListener 字段删除或仅作主 scope 兼容别名。

destroyModule(rootId):bridge 调用改为按 session.scopeKey 取 ctx 的 bridge(现状直接 mEngineContext.getBridgeManager())。

5.4 渲染回调 listener 路由(已知交错点)

onFirstPaint/onFirstContentfulPaint 无 rootId 参数(C++ FrameworkProxy 接口签名限制,renderer_native/FrameworkProxy.ets:22-24),回调落在 manager 单槽位。本次改法:取 mLastSession.moduleListener(最近一次加载卡片的 listener),主 scope 单卡行为与现状一致;多 scope 并发渲染时 listener 可能交错 —— 列为已知限制,后续若需精确路由,扩展 C++ 渲染回调携带 rootId(NativeRenderProviderManager::SaveRootIdWithScopeId 反查已就绪)。

5.5 EnvAPI 全局 env 状态(flexui_framework/modules/javascript/EnvAPI.ets)

EnvAPI 是框架级全局配置(类似 flexui_jsfwk),不区分 scope:

// ETS 侧保存 env 全量状态;每个 scope 的 bridge 就绪时推送完整状态(replaceEnv)
let _envState: Map<string, Object> = new Map()
let _bridgeManagers: Map<string, FlexUIBridgeManager | null> = new Map()

export function _setBridgeManager(mgr: FlexUIBridgeManager | null, scopeKey: string): void
//   scope 就绪时:if (mgr && mgr.isInitialized() && _envState.size > 0) → replaceEnv(全量) 到该 scope

export class EnvAPI {
  static setEnv(key: string, value: Object): void          // 更新 _envState + 广播 updateEnv 到所有已就绪 scope
  static setEnvObject(env: Record<string, Object>): void   // 替换 _envState + 广播 replaceEnv 到所有已就绪 scope
}
  • 保证每个 scope 的 env 一致:后创建的 scope 在其 bridge 就绪时由 _setBridgeManager 从 _envState 补推完整状态;已就绪的 scope 由 setEnv/setEnvObject 实时广播
  • 无 pending 队列:未就绪的 scope 不需要缓存操作,就绪时自动补齐
  • 调用点:FlexUIEngineManagerImpl.initBridgeCallback 按 scopeKey 注入;destroyEngine/destroyScope 按 key 置空

5.6 FlexView 关联(flexui/FlexView.ets)

/** 关联的 Engine key(必填:FlexUIEngine.createEngine 注册的 key,由调用方维护默认值)。 */
engineKey: string;
/** 关联的 Scope key(必填:卡片运行环境/globalThis,由调用方维护默认值)。 */
scopeKey: string;
  • aboutToAppear:FlexUIEngine.get(this.engineKey)(key 必填);未初始化时日志提示(call FlexUIEngine.createEngine() first);
  • onReady(() => loadCardInternal(), this.scopeKey);
  • loadCardInternal:loadCard(..., this.scopeKey);
  • 渲染绑定统一走 scope API(主/非主 scope 无分支,主 scope 的 key 也直接 getScopeContext('default') 命中注册表):
    • FlexViewNodeController.setRootView(rootView, engine, scopeKey):engine.getScopeContext(scopeKey)?.getLibFlexUI() + engine.getScopeNativeRenderProvider(scopeKey)?.getInstanceId(),避免跨 scope 绑定 root 到错误 render instance;
    • reload / updateDimension / aboutToDisappear 同款统一。

六、生命周期

createEngine(config)                 → 创建主 scope(key 由调用方传入),initBridge → core bundle → api bundles → engine ready
  └─ createScope('sandbox', cb)    → new ContextImpl(新 JsDriver/C++ scope/context)→ initBridge → core bundle → api bundles → scope ready
      └─ loadCard(url,..., 'sandbox') → createRootView(sandbox 的 provider)→ loadInstance(sandbox 的 bridge)
          └─ FlexView(engineKey, scopeKey='sandbox') → 渲染绑定 sandbox 的 render provider
destroyScope('sandbox')            → destroyBridge + destroy(释放 native 资源)→ 注册表移除
destroyEngine/destroy('default')   → 遍历全部 scope 销毁 → 注册表移除

七、风险与代价

项 说明 缓解
jsfwk 每 scope 重跑 bootstrap + flexui_jsfwk.js + api bundles per-scope,内存/启动成本线性增长 code cache 已生效;业务侧按需建 scope,不用不建
codeCacheTag 互踩 多 scope 同 tag 写同一缓存文件 5.3 派生 tag(${tag}_${scopeKey})
渲染回调 listener 交错 onFirstPaint 无 rootId 取最近 session(5.4),后续扩展 C++ 签名
EnvAPI env 操作 静态默认主 scope 隔离需求下 env 属全局配置,语义合理;如需要可后续加 scopeKey 参数
Devtools 多 scope inspector 按 scope 绑定(C++ 已支持) 验证时逐 scope 调试
遗留 root 路由 动画/ui_layout_module 仍走 scope->GetRootNode()(单槽位,多 scope 各自独立更安全) 不阻塞本次;后续按 SceneBuilder 思路统一

八、实施与验证

  1. facade(flexui/FlexUIEngine.ets):注册表 + config 扩展 + scope API 透传 + bundles 抽取;
  2. native engine(FlexUIEngineManagerImpl.ets):mScopes、createScopeInternal、路由、session 扩展;
  3. EnvAPI.ets:Map 化;
  4. FlexView.ets:engineKey/scopeKey;
  5. 编译验证:./build.sh build-engine(.ets 改动走 hvigor);
  6. 真机验证:默认路径回归(createEngine({engineKey:'default', groupId:1000}) + FlexView 必选属性)+ 多 scope 场景(createScope('s1') 后两张卡片分别绑 'default'/'s1',验证 globalThis 隔离:卡片 A 挂全局变量,卡片 B 不可见)。