FlexUI OHOS 运行时隔离现状

日期: 2026-08-05 | 分支: ai | 平台: OHOS (HarmonyOS) | 关联: docs/bugfix/2026-08-05-multicard-scenebuilder-root-routing.md

本文梳理 flexui-engine 在 OHOS 上 JS 运行时的隔离机制现状:JSVM/Engine/Scope/Context/Instance 五个层级的关系、groupId 复用、globalThis 共享边界、flexui_jsfwk.js 的管理,以及多卡片场景的隔离粒度与 root 路由修复。

一、隔离层级模型

JSVM (JSHVM)            OH_JSVM_CreateVM 创建的 ArkTS JSVM 实例(≈ V8 Isolate,独立堆/GC)
  └─ Engine             持有 VM + JS task runner + class templates,按 groupId 复用
       └─ Scope         每次 JsDriver_CreateJsDriver 一个;引擎"运行会话"封装
            └─ Context  每个 scope 一个独立 globalThis(JSVM_Env)
                 └─ Instance  同一 scope 内多张卡片(rootView),逻辑隔离
概念 是什么 谁创建 数量关系
JSVM ArkTS 运行时实例 JSHVM 构造(driver/js/src/vm/jsh/jsh_vm.cc) 可多个(按 groupId)
Engine VM + JS 任务线程 + class 模板注册表 Engine::CreateVM(driver/js/src/engine.cc) 按 groupId 复用
Scope 一个 Context 及配套引擎服务(bridge/module 实例/性能/uri loader) Engine::CreateScope(每次 CreateJsDriver 一个) Scope 数 = Context 数
Context 独立全局环境(globalThis) Scope::CreateContext → VM::CreateContext(driver/js/src/scope.cc:210) 与 Scope 严格 1:1
Instance 卡片(rootView / loadInstance) __loadInstance__({name, id})(driver/js/lib/global/Event.js:12) 一 scope 内多张

Scope 与 Context 是严格的 1:1

  • scope.h 只有单一 context_ 成员(driver/js/include/driver/scope.h:500),GetContext() 直接返回
  • 创建路径唯一:Scope::CreateContext(scope.cc:210)→ engine->GetVM()->CreateContext(),无"注入已有 context"的入口
  • JSH 上每次创建对应全新 JSVM_Env(jsh_ctx.cc:228 OH_JSVM_CreateEnv),析构时 OH_JSVM_DestroyEnv
  • 生命周期绑定:Scope 析构即销毁 Context;无独立 context 管理器
  • 共享只发生在 VM 层:N 个 Scope(各自 1 个 Context)可共享同一个 JSVM(reuse_engine_map 按 groupId 复用)

二、groupId 复用机制(Engine/JSVM 层)

JsDriverUtils::CreateEngineAndAsyncInitialize(task_runner, param, group_id, is_reload)(driver/js/src/js_driver_utils.cc:189)维护全局 reuse_engine_map<groupId, (Engine*, 引用计数)>:

groupId 行为
-1(kDefaultGroupId,js_vm.h:19) 每次创建新 Engine(新 JSVM),不复用
-2(kDebuggerGroupId) 调试模式;reload 复用,否则新建
其他值(OHOS 用 1000 = FLEXUI_GROUP_ID) 同组共享同一 Engine/JSVM,引用计数 +1;归零才销毁 VM

销毁对称逻辑在 DestroyInstance(js_driver_utils.cc:593)。

三、OHOS 当前部署形态(单例收敛)

1 个 JSVM(JSHVM,OH_JSVM_CreateVM × 1)
1 个 FlexUIEngine(TS 单例 ensureInit,FlexUIEngine.ets:143-168)
1 个 FlexUIEngineContextImpl(每 engine 一个,FlexUIEngineContextImpl.ets:35)
1 个 JsDriver → 1 个 Scope → 1 个 Context(globalThis)
N 张卡片(rootView),共享上述全部
  • FlexUIEngine.ensureInit 是进程内单例(sInstance),params.groupId = 1000(FlexUIEngine.ets:328)
  • 每 engine 一个 JsDriver(scope),所有卡片 loadCard → loadModuleWithListener → loadInstance(rootId) 在同一 scope 同一 context 上运行
  • C++ 层天然支持多 VM(不同 groupId)/多 context(多次 CreateJsDriver),是 TS 封装层把它收敛成单例

四、root 机制(渲染树归属)

  • RootNode 本体:进程级全局注册表 RootNode::PersistentMap()(dom/include/dom/root_node.h:105,PersistentObjectMap<uint32_t, shared_ptr<RootNode>>),按 root_id 管理;每卡片一棵独立 DOM 树。创建:DomManager_CreateRoot(dom_manager_napi.cc:100);销毁:DestroyRoot(Erase)
  • scope 的 root 是单槽位:scope.h:513 std::weak_ptr<RootNode> root_node_,每次 createRootView → JsDriver.attachToRoot(rootId)(js_driver_napi.cc:398 JsDriver_SetRootNode)被覆盖为最新卡片;不维护多 root
  • root_id → scope_id 反查:NativeRenderProviderManager::SaveRootIdWithScopeId(native_render_provider_manager.cc:22),渲染回调(首帧/FCP/资源打点)按 root 反查 scope
  • DomManager 共享:一个 engine 一个 DomManager,SetDomManager 挂到每个 root(dom_manager_napi.cc:146);Scope::GetDomManager() 在 OHOS 分支从 root 反查

多卡片 DOM 写回路由(2026-08-05 修复)

修复前:JS 侧 new FlexUI.SceneBuilder() 无 root 参数,C++ 回调一律 scope->GetRootNode() → 所有卡片 DOM 命令打到最后 attach 的 root → 卡片间串扰(第二张卡片起无法渲染)。

修复后(详见 bugfix 文档):

  • C++ SceneBuilder 增加 root_node_ 绑定,constructor 接收可选 root_id,从 RootNode::PersistentMap() 解析(scene_builder_module.cc:435-456)
  • 7 个方法回调统一走 ResolveSceneBuilderTarget(绑定 root 优先,fallback scope 当前 root;scene_builder_module.cc:243-262)
  • JS 6 个入队函数加 rootId 参数,NativeBackendElement 全部调用点显式传 _context.rootViewId;flushQueue 按 rootId 分组,每组一个绑定 root 的 SceneBuilder
  • reserveRootId 单值改 Set(多卡 root id 全保留)

五、flexui_jsfwk.js 与 JS 框架管理

  • 构建:scripts/build-jsfwk(webpack)把 @openflexui/core + @openflexui/backend + @openflexui/frontend 打成 flexui_jsfwk.js(UMD,挂 globalThis.__flexui__),产物在 framework/ohos/src/main/resources/rawfile/(不入 git)
  • 加载时机:每个 scope 初始化完成后执行一次——initBridge → JsDriver_CreateJsDriver(C++ 建 VM+Context,跑内嵌 bootstrap.js,scope.cc:234)→ 初始化回调里 runEngineScriptFromUri(flexui_jsfwk.js)(FlexUIBridgeManagerImpl.ets:263)→ 按序加载 apiJSAssetsPaths(has_apis.js、agent_api.js 等,FlexUIEngine.ets:385-406)
  • 框架代码每 context 独立一份:webpack 模块闭包、require 缓存、框架实例状态均 per-context;同 scope 内卡片 bundle 用 mLoadedBundleInfo 去重(FlexUIBridgeManagerImpl.ets:313)
  • code cache:core bundle 支持字节码缓存,失败累计 3 次自动清除(js_driver_utils.cc CheckUseCodeCacheBefore/AfterRunScript)

六、globalThis 共享边界

  • 同一 FlexUIEngine(默认形态):所有卡片共享同一个 globalThis。__GLOBAL__(含 appRegister)、FlexUI、global.flexuiBridge、__flexui__、jsModuleList、框架模块闭包全部共享;卡片之间无环境级隔离,任一张卡片的全局污染(挂变量/改原型)会影响全部
  • 不同 Engine(不同 groupId 或 -1):不同 Scope → 不同 Context → globalThis 完全隔离
  • 动态加载(runScriptFromUri、ContextifyModule.LoadUntrustedContent)也执行在当前 scope 的 context 上(contextify_module.cc:115),与卡片共享 globalThis

七、隔离粒度结论

当前 OHOS 的隔离是**"instance(卡片)级逻辑隔离 + rootId 级渲染隔离"**,不是环境(context/VM)级沙箱:

维度 是否隔离 机制
JSVM/堆 否(共享 1 个) groupId=1000 复用
Context/globalThis 否(共享 1 个) 单例 engine + 单 JsDriver
卡片 JS 状态 部分(逻辑层) __instanceId__/__instanceName__ 路由(Event.js:12)
DOM 树/渲染 是 RootNode::PersistentMap + root_id 路由(2026-08-05 修复)
事件回调 是 事件在正确 root 的树上触发,listener 按 dom_id 注册

若需每卡片独立 context/VM(方案 2):C++ 层支持(多 scope/多 groupId),代价是 flexui_jsfwk.js 每 context 重跑、内存/启动成本上升,TS 封装层需改造(JsDriver 创建时机、卡片↔rootView 绑定、native 模块 per-scope 注册等)。

八、遗留问题

  1. 动画模块(animation_module.cc/animation_frame_module.cc)与 ui_layout_module.cc 仍走 scope->GetRootNode()——多卡片动画会打错 root,需按 SceneBuilder 同款思路加 root 路由
  2. callUIFunction JS 链路缺口:FlexUI.bridge 上没有 callUIFunction 方法(FlexUIBridge.ts:578 调用会抛错被 try/catch 吞掉);C++ 侧 root 支持已就位,JS 侧调用链需另案修通
  3. has.*Sync storage 失败:StorageAPI 的 *Sync 方法执行成功(resolve 被调用)且 ASAPIsModule.call 已 return promise,但运行日志显示 [ASCF Main]: ASAPIsModule --> handleCallSync: result=null——存在工作区外适配层(NativeModuleAdapter,源码不在仓库内)吞掉同步结果,导致 skill 的 storage 调用失败、推荐卡片空数据;需先定位该适配层来源(疑为 ASCF Shared Library / @atomicservice/ascf-base)