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:228OH_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:513std::weak_ptr<RootNode> root_node_,每次createRootView → JsDriver.attachToRoot(rootId)(js_driver_napi.cc:398JsDriver_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.ccCheckUseCodeCacheBefore/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 注册等)。
八、遗留问题
- 动画模块(
animation_module.cc/animation_frame_module.cc)与ui_layout_module.cc仍走scope->GetRootNode()——多卡片动画会打错 root,需按 SceneBuilder 同款思路加 root 路由 callUIFunctionJS 链路缺口:FlexUI.bridge上没有callUIFunction方法(FlexUIBridge.ts:578调用会抛错被 try/catch 吞掉);C++ 侧 root 支持已就位,JS 侧调用链需另案修通has.*Syncstorage 失败:StorageAPI的*Sync方法执行成功(resolve 被调用)且ASAPIsModule.call已return promise,但运行日志显示[ASCF Main]: ASAPIsModule --> handleCallSync: result=null——存在工作区外适配层(NativeModuleAdapter,源码不在仓库内)吞掉同步结果,导致 skill 的 storage 调用失败、推荐卡片空数据;需先定位该适配层来源(疑为 ASCF Shared Library / @atomicservice/ascf-base)