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 渲染层,任一张卡片的全局污染会影响全部。
需求(用户决策):
- 移除单例设计,统一显式创建:删除
ensureInit/sInstance/instance,createEngine为唯一创建入口;engineKey/groupId/scopeKey 全部必选,引擎内部不做默认值兜底,由调用方自行维护默认 key/id 常量。 - 多 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/instancegetter)已按需求移除,引擎不再有默认 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?):
const ctx = scopeKey ? this.mScopes.get(scopeKey) : (主 scope);不存在 → 报错返回 null(提示先 createScope);- 卡片 loader 创建逻辑不变(per-card
FlexUIAssetBundleLoader),codeCacheTag 派生:loadParams.codeCacheTag非空时追加'_' + scopeKey,避免多 scope 写同一字节码缓存文件; ctx.createRootView(loadParams)(注意:现状用this.mEngineContext!,改为目标 ctx);- session 存
cardSessions,同时更新mLastSession(兼容 debug reload 路径); ctx.setRootView/ setLoadModuleListener/ setComponentName(per-scope 槽位,天然隔离);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 思路统一 |
八、实施与验证
- facade(
flexui/FlexUIEngine.ets):注册表 + config 扩展 + scope API 透传 + bundles 抽取; - native engine(
FlexUIEngineManagerImpl.ets):mScopes、createScopeInternal、路由、session 扩展; EnvAPI.ets:Map 化;FlexView.ets:engineKey/scopeKey;- 编译验证:
./build.sh build-engine(.ets 改动走 hvigor); - 真机验证:默认路径回归(
createEngine({engineKey:'default', groupId:1000})+ FlexView 必选属性)+ 多 scope 场景(createScope('s1')后两张卡片分别绑 'default'/'s1',验证 globalThis 隔离:卡片 A 挂全局变量,卡片 B 不可见)。