鸿蒙跨端实践-揭秘视图渲染流程

鸿蒙跨端实践-揭秘视图渲染流程

文章图片

鸿蒙跨端实践-揭秘视图渲染流程

文章图片

鸿蒙跨端实践-揭秘视图渲染流程

文章图片

鸿蒙跨端实践-揭秘视图渲染流程
作者:京东科技 刘宁
一、前言

通过本文你将全面清晰的洞悉动态化跨端的实现原理 , 感受黑悟空(数据)一路打怪升级(在不同语言环境中流转改造) , 逆天改命(操控原生视图绘制) , 终成齐天大圣(完成视图渲染呈现)的艰辛历程 。
二、原理介绍1.动态化跨端原理介绍动态化- 罗码(Roma , 后文统称动态化)是一个完全自主研发的一站式跨平台解决方案 , 一份代码 , 可以在 Android、iOS、Harmony 及 Web 上运行 。 动态化的跨端原理与 React Native 、Weex 一致 , 在吸取业界各跨端框架优势的基础上 , 加上自主创新 , 打造的一个完全自主可控的综合跨端解决方案 。 其跨端的理论基础在于各端都具备统一解析和执行 JavaScript 的能力(JS 虚拟机) 。 业务代码被打包成 js 文件 , 在各端被 JS 虚拟机加载 , 被解析成多条不同功能的指令 , 调用原生宿主能力 , 完成数据的传递和视图的绘制 。 示意图如下:



??


为了在鸿蒙端更好的接入动态化 , 有几点需要特别说明一下:
1.鸿蒙系统的方舟虚拟机能直接加载js文件吗?
鸿蒙的开发语言 ArkTS , 是js语言的超集 , 但为了获取更高的执行效率去除了js语言的动态性 , 是强类型的编程语言 , ets 文件会被编译为 abc 文件 (方舟字节码文件) , 再由方舟虚拟机加载运行 。 也就是说方舟虚拟机只能加载和运行 abc 文件 , 而无法直接加载 js 文件 。 这为动态化在鸿蒙端的应用带来了很大的挑战 , 我们一边尝试把 V8 移植到鸿蒙(可参考 【史无前例 , 移植V8虚拟机到纯血鸿蒙系统】) , 一边与华为的工程师沟通建议将 V8 内置到鸿蒙系统中 。 最终 , 华为采纳了我们的建议 ,提供了一套 JSVM-API , 提供了较为完整的 JS 引擎能力 , 为动态化在鸿蒙端的适配提供了理论支持 。
2. JS 虚拟机能高效的执行动态化相关指令吗?
为了让不同业务场景下的动态化产物高效地被 JS 虚拟机解析执行 , 需要扩充 JS 虚拟机的能力 , 提供适合动态化跨端场景下的功能 , 包括实例的创建和管理 , 视图的增、删、改、查以及便捷的跨语言通讯等功能 。 当然 , 为了使各平台更好的接入和使用动态化能力 , 动态化提供了一套统一接口 , 各平台依靠自身原生能力实现即可 。
3.鸿蒙端的 App 如何接入动态化?
为了让 App 更好的接入动态化 , 我们提供了鸿蒙端的静态库文件 libRomaSdk.so (后文简称 SDK , 由 NDK 通过 CMake 和 Ninja 将 C/C++ 代码编译而得来 , 这个过程可参考官网 , 这里不展开介绍) , 只要在工程中配置相关依赖即可使用动态化能力 。
2.鸿蒙接入动态化原理介绍动态化在鸿蒙端的架构示意图如下:



??


App启动时第一时间就会加载动态化 SDK , 包括 JS Engine(动态化自主研发的为各平台的 JS 虚拟机扩展的功能) 和 Jue Instance (鸿蒙端对 JS Engine Interface 的实现)两部分 , 完成动态化运行环境的初始化 。 具体包括业务代码实例管理、任务管理、虚拟Dom树管理、虚拟Dom树 Differ、业务页面渲染管理、生命周期管理、事件分发、业务逻辑处理等一系列功能 。
为了适配不同系统 , 通过制定统一的对外接口规范(JS Engine Interface)让不同平台按照标准化对接及扩展(比如这次华为鸿蒙系统的适配只需实现统一对外接口即可) , 接口包含了实例创建、生命周期、元素的增删改查、JS和原生的双向通讯、热重载交互等各种能力 , 各端按需实现 。
鸿蒙系统上 , Jue Instance 具体实现接口声明的方法 , 创建动态化页面和处理业务交互逻辑 。 提供内置基础标签和模块 , 同时打通了特殊业务场景下的标签和模块的扩展能力 , 业务可根据具体的业务场景扩充和完善 。
在 JS Engine 环境中通过对视图元素的增删改查操作及其他相关指令构建出了一个组件树(后文统称 V-Dom Tree ,作为一个纯对象 , 可以清晰的提炼出视图元素的层级结构 , 这个抽象描述是与平台无关的 , 因此可在 JS环境中生成 V-Dom Tree) , 在原生端根据 V-Dom Tree 的结构创建出对应的 Render Tree , 再经过布局引擎布局后 , 就显示出了业务具体的视图效果;由于通过统一接口实现了 JS 和原生的双向通讯 , 当用户在业务视图上触发了交互事件 , 或业务方需要改变视图显示的时候 , 都可通过统一接口顺利的完成事件和数据的交互逻辑以及业务视图的渲染 。
三、业务示例以在京东金融中加载一个简单的动态化页面为例 , 来分析视图的加载和更新流程 。 创建一个如下所示的动态化页面 , 点击页面中“更新节点数据”按钮 , 修改图片为一张新图片 , 并且将一个子视图的背景色修改为红色 。 鸿蒙端效果如下所示 。



??


1.业务代码展示首先看动态化代码 , 按照前端标准的三段式风格 , 上手开发很容易 。 新建 ADemo.jue 文件( Jue 是我们自定义的开发语言 , 语法几乎和 Vue 一致 , 但扩展了新增标签的能力) , 代码如下:
<template style=\"border-width: 2px;\"><div class=\"normalClass\" style=\"margin-top: 100px;\"><image class=\"normalClass\" :src=https://mparticle.uc.cn/"imageUrl\" style=\"height: 200;\"></image><div class=\"normalClass\" :style=\"{'background-color':bgColor\" style=\"height:60px;\"><textstyle=\"border-width: 2px;width:260px;height:40px;align-self: center;text-align: center;background-color: white;\"@click=\"change()\">更新节点数据</text></div></div></template><script>export default {data() {return {bgColor: \"white\"imageUrl: \"https://static.foodtalks.cn/company/images/434/121logo.png\"mounted() {methods: {change() {this.bgColor = \"red\";this.imageUrl = \"https://www.szniego.com/uploads/image/20210202/1612232326.png\";this.updateInstance();</script><style scoped>.normalClass {margin: 10px;justify-content: center;align-items: center;align-self: stretch;border-width: 2px;</style>2.资源加载流程通过平台线上打包或者使用动态化提供的脚手架打包命令 , 将 ADemo.jue 打包成 ADemo.zip , 通过资源管理平台下发至客户端设备 , 经过解压、解密等手段获取 ADemo.js 等文件 , 原生在合适的时候创建动态化视图 , 并通过 JS 虚拟机加载 ADemo.js 文件 , 从而开始视图绘制 。
动态化资源从打包到被 JS 虚拟机加载的流程图如下:



??


3.产物代码展示下面我们来看一下 ADemo.js 文件的具体内容 , 截取主要代码如下:
/******/ (function(modules) { )/************************************************************************//******/ ({/***/ \"./src/jueDemoList/ADemo/ADemo.jue\":/*!*******************************************!*\\!*** ./src/jueDemoList/ADemo/ADemo.jue ***!\\*******************************************//*! exports provided: default *//***/ (function(module __webpack_exports__ __webpack_require__) {\"use strict\";__webpack_require__.r(__webpack_exports__);var template = JRTemplateManager._jr_create_jue_template('ADemo.jue' {\"id\": \"ADemo\"\"version\": \"11\"\"dependencies\": {\"JSEngine\": \"0.9.4\");JRTemplateManager._jr_create_t_node('ADemo.jue' '0' '40e129af-5e6a-70b8-a757-28a22785dc2f' 'document' {\"style\": \"border-width: 2px;\");JRTemplateManager._jr_create_t_node('ADemo.jue' '40e129af-5e6a-70b8-a757-28a22785dc2f' '918d8bb8-9362-bcd6-00ee-35b85c435072' 'div' {\"class\": \"normalClass\"\"style\": \"margin-top: 100px;\");JRTemplateManager._jr_create_t_node('ADemo.jue' '918d8bb8-9362-bcd6-00ee-35b85c435072' '9e848985-59ac-bd8b-e85a-617a6e9a08dd' 'image' {\"class\": \"normalClass\"\":src\": \"imageUrl\"\"style\": \"height: 200;\");JRTemplateManager._jr_create_t_node('ADemo.jue' '918d8bb8-9362-bcd6-00ee-35b85c435072' 'b56d24a9-c08e-6b32-9558-162f2aece68d' 'div' {\"class\": \"normalClass\"\":style\": \"{'background-color':bgColor\"\"style\": \"height:60px;\");JRTemplateManager._jr_create_t_node('ADemo.jue' 'b56d24a9-c08e-6b32-9558-162f2aece68d' '440bad33-f8bb-6baf-fe4f-b9bf768d4cc1' 'text' {\"style\": \"border-width: 2px;width:260px;height:40px;align-self: center;text-align: center;background-color: white;\"\"@click\": \"change()\");JRTemplateManager._jr_add_t_node_value('ADemo.jue' '440bad33-f8bb-6baf-fe4f-b9bf768d4cc1' 'value' '更新节点数据');var __default__ = {data: function data() {return {bgColor: \"white\"imageUrl: \"https://static.foodtalks.cn/company/images/434/121logo.png\";mounted: function mounted() {methods: {change: function change() {this.bgColor = \"red\";this.imageUrl = \"https://www.szniego.com/uploads/image/20210202/1612232326.png\";this.updateInstance();;template.script = __default__;__default__.filename = 'ADemo.jue';__default__.__template__ = function () {return template;;/* harmony default export */ __webpack_exports__[\"default\"
= (__default__);template.globalStyle = {;template.style = {\".normalClass\": {\"margin\": \"10px\"\"justify-content\": \"center\"\"align-items\": \"center\"\"align-self\": \"stretch\"\"border-width\": \"2px\";/***/ )/******/ );//# sourceMappingURL=ADemo.js.map
可以看到页面数据都被封装到一个自执行函数中 , 在加载资源文件时 , 此函数就会被调用 。 函数内页面数据都被保存到__default__变量中 , 视图以及视图层级关系则被解析成一条条节点相关的指令 。
四、视图绘制流程1.绘制原理在鸿蒙端 , 进入动态化页面之前 , 确保资源文件(ADemo.js)已被加载到内存中后 , 由原生端(ArkTS)发起动态化实例的创建 , 调用动态化 SDK 提供的创建动态化实例(Instance)的方法 , 通过 N-API 调用宿主环境(C++)的具体实现 , 创建 C++ 环境下的 Instance 。 然后通过 JSI (将C++ 中的常用类型与 JavaScript 中的类型一一对应 , 可互调方法和操作对象 。 从而消除了数据序列化和线程切换调用的开销 , 极大提升通讯性能)调用预置到 JS 虚拟机中的 JS Engine 的接口方法 , 创建 JS 环境下的 Instance 。在 JS 环境中 , 收集页面信息 , 完成 V-Dom Tree 的创建 , 再通过 JSI 将页面数据传递给宿主环境(C++) , 根据 V-Dom 的结构创建组件树(Component Tree)再通过 N-API 将数据传递给原生 , 调用 ArkTS 构建对应的渲染树(Render Tree) , 完成视图绘制 。



??


上图展示了动态化在鸿蒙端绘制页面的过程 , 总结一下就是三种语言环境 , 三个 Instance , 三个 Thread。 三个 Instance 独立且一一对应 。 ArkTs 中的 Instance 在 UI 线程中用来完成页面的绘制 , 数据的绑定和事件的触发等 。 C++ 中的 Instance 作为数据的中转存储和事件转发 , 在 bg Thread 处理复杂耗时的逻辑 , 避免阻塞 UI Thread 和 js Thread 。 JS 中的 Instance 负责搜集产物信息 , 构建 V-Dom Tree , 传递数据和事件等都在 js Thread 中处理 。 C++ 和 ArkTs 中根据 V-Dom Tree 创建 Component Tree 和 Render Tree 。
通过查看 ADemo.js 资源文件 , 可以得出 V-Dom Tree 的结构 , 以及对应的 Component Tree 和 Render Tree 。 三者的结构如下图所示:



??


2.代码分析下面截取部分核心代码 , 分析动态化页面的创建过程 。 首先在原生端进入动态化页面 , 创建 RomaInstanceView 对象 , 首先触发 aboutToAppear 方法 , 准备创建 ArkTS/C++/JS 三种语言环境下的 Instance 实例 。
public aboutToAppear() {// 确保获取资源js文件并加载到内存中romaAssetsManager.ensureAsset(this.jueName! progress).then((version) => {// 完成不同语言环境下Instance实例的创建// 1 创建 arkTS 和 cpp 实例this.romaInstance = this.createInstance(this.rootContent this.pageId);// 2 创建 JS 实例this.romaInstance!.startInstance(this.initialProps).then((result) => {);).catch((error: Error) => {)1.创建 ArkTS 环境中的 Instance 实例通过调用 createInstance 方法创建 ArkTS 环境下的 Instance 实例 , 并传入 stateListener 参数监听 Instance 在C++ 语言环境中的创建过程 , 包括开始创建实例、创建完成和更新完成的状态 。
private createInstance(root: NodeContent pageId: string | null): RomaInstance {this.shouldDestroyRomaInstance = truereturn RomaEnv.createAndRegisterRomaInstance(this.jueName! {root: rootuiContext: this.getUIContext()abilityContext: getContext(this) as common.UIAbilityContextpageId: pageIderrorListener: this.errorListener ? (error) => {this.errorListener?.(error);console.warn(\"CreateInstanceView\" error.message);this.pageStage = PageStage.ERROR: undefinedthis.stateListener);// 类型public stateListener?: RomaStateListener;export type RomaStateListener = (instanceId: string state: \"createFinish\" | \"updateFinish\" | \"createInstance\"romaInstance: RomaInstance) => void;createAndRegisterRomaInstance 方法内直接调用 createInstance 方法完成 ArkTs 环境中 Instance 的创建 , 并绑定实例状态变化的监听 。
public createInstance(jueName:string param: RomaInstanceParam stateListener?:RomaStateListener): RomaInstance {const id = ++this.nextInstanceId;// 创建 arkTS 侧的实例const instance = new RomaInstance(jueNameid.toString()\"\"this.napiBridgeparam.uiContextparam.abilityContext)// 给实例绑定状态变化的监听if (stateListener) {instance.addStateListeners(stateListener);instance.setPageId(param.pageId);instance.initialize(param.root);this.instanceMap.set(id.toString() instance);return instance;2.创建 C++ 环境中的 Instance 实例在调用 instance.initialize 中通过 N-API调用 crateInstance , 准备在 C++ 环境中创建 Instance 。 其中第4个参数 (mutations: Mutation[
isFromCore: boolean) => { this.descriptorManager.applyMutations(mutations isFromCore)用来监听 C++ 环境中 Instance 的创建状态 。 当后面分析到 C++ 环境中实例创建完成时 , 我们在分析 this.descriptorManager.applyMutations(mutations isFromCore) 中具体做了什么 。 这里只需要知道 ArkTs 中的 Instance 在监听 C++ 中的 Instance 的创建过程 。
public initialize(root: NodeContent | null = null) {// 注册实例变化监听器this.napiBridge.createInstance( this.jueName this.getId() root(mutations: Mutation[
isFromCore: boolean) => {this.descriptorManager.applyMutations(mutations isFromCore)(tag commandName args) => {// 省略无关代码(instanceId: string state: string) => {// 省略无关代码)
通过 N-API 在 C++ 环境中创建对应的实例 , 并在创建 Instance 的回调中触发从 ArkTS 环境中传递过来的状态监听 ,auto listener = arkJs.getReferenceValue(listener_ref); arkJs.call<1>(listener args); 从而将 C++ 中 Instance 创建过程传递到 ArkTs 的 Instance 中 。
static napi_value createInstance(napi_env env napi_callback_info info) {ArkTS arkJs(env);auto args = arkJs.getCallbackArgs(info 6);auto jueName = arkJs.getString(args[0
);InstanceId instanceId = arkJs.getString(args[1
);ArkUI_NodeContentHandle nodeContentHandle_;if(!arkJs.isUndefined(args[2
)){OH_ArkUI_GetNodeContentFromNapiValue(env args[2
&nodeContentHandle_);auto listener_ref = arkJs.createReference(args[3
);auto componentMethod_ref = arkJs.createReference(args[4
);auto stateListenerMethod_ref = arkJs.createReference(args[5
);auto &engine = RomaEnv::getInstance();engine.createInstance(jueName instanceId nodeContentHandle_[env listener_ref
(MutationsToNapiConverter mutationsToNapiConverter auto const &mutations) {// C++环境中 Instance 状态变化时触发if (mutations.size() <= 0) {return;ArkTS arkJs(env);auto napiMutations = mutationsToNapiConverter.convert(env mutations);std::array<napi_value 1> args = {napiMutations;// 获取从 ArkTS 环境中传递过来的监听对象并触发auto listener = arkJs.getReferenceValue(listener_ref);arkJs.call<1>(listener args);return arkJs.getUndefined();
在 C++ 环境中创建 Instance 的具体实现如下 , 记录下从 ArkTs 中获取的状态监听参数 mutationsListener 以便在合适的时机触发 。
void RomaEnv::createInstance(std::string jueName InstanceId instanceId ArkUI_NodeContentHandle nodeContentHandle_ MutationsListener mutationsListenerComponentMethodListener componentMethodListener StateListener stateListener) {RomaEnv::getInstance().getBackgroundExecutor()([=
() {auto nonConstRef = RomaInstanceManager::getInstance().get(instanceId);// 创建实例 , 页面 | 模板RomaInstance::Shared instance = std::make_shared<RomaInstance>(jueName instanceId);instance->rootInstance_ = instance;instance->attachNativeXComponent(nodeContentHandle_);// 注册监听器instance->registerInstanceChangeListener(mutationsListener);……);
3.创建 JS 环境中的 Instance 实例下面分析 JS 端 Instance 的创建过程 , 接上面 this.romaInstance!.startInstance(this.initialProps).then((result) => {); 往下看
public async startInstance(initialProps: TObject): Promise<void> {return this.napiBridge.startInstance(this.getId()initialProps);在 ArkTs 端通过 N-API 调用 SDK 中的 startInstance 进入 C++环境
static napi_value startInstance(napi_env env napi_callback_info info) {ArkTS arkJs(env);arkJs.methodName = \"startInstance\";auto args = arkJs.getCallbackArgs(info 3);InstanceId instanceId = arkJs.getString(args[0
);auto onFinishRef = https://mparticle.uc.cn/api/arkJs.createReference(args[2
);auto &engine = RomaEnv::getInstance();engine.startInstance(instanceId arkJs.getDynamic(args[1
) [env onFinishRef
() {ArkTS arkJs(env);auto listener = arkJs.getReferenceValue(onFinishRef);arkJs.call<0>(listener {);arkJs.deleteReference(onFinishRef););return arkJs.getUndefined(); // startInstance 的具体实现void RomaEnv::startInstance(InstanceId instanceId folly::dynamic &&initialPropsstd::function<void()> &&onFinish) {try {RomaEnv::getInstance().getBackgroundExecutor()([=
() {auto nonConstRef = RomaInstanceManager::getInstance().get(instanceId);if (nonConstRef) {// 准备进入 js 环境创建 InstancenonConstRef->start(instanceId initialProps);this->taskExecutor_->runTask(TaskThread::MAIN [onFinish
() {onFinish();););catch (const std::exception &e) {throw e.what();;
在 start 方法中开始创建 js 环境下的 Instance , 通过 JSI 获取 JS 虚拟机对象 runtime, 调用在 App 启动时就注入到 JS 虚拟机中的 JRPageManager 对象的 createInstance 方法
voidRomaInstance::start(SurfaceId surfaceId folly::dynamic const &initialProps){// 进入js线程执行实例创建RomaEnv::getInstance().getRuntimeExecutor()([jueName_ = jueName_initialProps surfaceId
(jsi::Runtime &runtime) {// 判断 JS 的全局变量中是否有 JRPageManagerif (runtime.global().hasProperty(runtime \"JRPageManager\")) {jsi::Object JRPageManager = runtime.global().getPropertyAsObject(runtime \"JRPageManager\");if (JRPageManager.hasProperty(runtime \"createInstance\")) {jsi::Function method = JRPageManager.getPropertyAsFunction(runtime \"createInstance\");method.callWithThis(runtime JRPageManager{jsi::valueFromDynamic(runtime jueName_+\".jue\")jsi::valueFromDynamic(runtime surfaceId)jsi::valueFromDynamic(runtime initialProps)););
进入 JS 环境后调用 createInstance 方法 , 开始创建 JS 环境下的 Instance
export function createInstance(bundleNameinstanceIDoptions){// 判断是否传入实例bundleName以及对应bundle是否已经加载if (!bundleName || !has_load_bundle(bundleName)) {callLoadBundleJsFileFail(instanceID)return;JRTransUICore._jr_ydby_new_template_instance(bundleNameinstanceIDoptions);在 JS环境中完成 Instance 创建后 , 调用 _jr_ydby_new_node_instance 开始构建 V-Dom Tree
function _jr_ydby_new_template_instance(template_id ctx_id template_data is_batchCreate) {// 1.根据模板创建JUE实例var ctx = new JueInstance(ctx_id template template_data);ctx.initRootCtxAndStaticCss();……// 创建v-domvar v_dom = new _jr_ydby_v_dom(template_id);ctx.v_dom = v_dom;// 2.构建v-dom 对应 root-nodevar root_node = _jr_ydby_new_node_instance(ctx.c_id v_dom template.root_node null { is_batchCreate);// 构建结束_jr_ydby_create_finshed(ctx.c_id);return ctx.c_id;4.在 JS 环境中构建 V-Dom Tree通过 _jr_ydby_new_node_instance 方法 , 遍历视图中所有节点及子节点 , 完成整个 V-Dom Tree 的创建
export function _jr_ydby_new_node_instance(ctx_id v_dom current_t_node parent_node v_f_ctx is_batchCreate itemIndex) {var cur_env = _jr_ydby_node_parse_jscontext(ctx_id v_f_ctx);let ctx = __jr_template__ctx[ctx_id
;var current_node = null;if (current_t_node.type === 'document') {// 创建根节点current_node = new JUE_NODE(v_f_ctx ctx_id \"\" current_t_node);current_node.setAttr(cur_env ctx)_jr_ydby_create_body(current_node);else{// 创建其他子节点current_node = new JUE_NODE(v_f_ctx ctx_id parent_node.id current_t_node);current_node.setAttr(cur_env ctx);current_node.setStyle(cur_env parent_node.style);current_node.setValue(cur_env v_f_ctx);current_node.setEvent(cur_env);// 组装数据 , 生成 _jr_ydby_v_node对象parent_node.appendChild(current_node v_dom);// 添加节点_jr_ydby_add_element(current_node current_node.node_index);// 循环创建当前节点下的子节点if (current_t_node.sub_nodes) {let v_if_else_parse_result = [
;for (let i = 0; i < current_t_node.sub_nodes.length; i++) {v_f_ctx = Object.assign({ v_f_ctx);let sub_t_node = current_t_node.sub_nodes[i
;let att = sub_t_node.attr;let v_for_value = https://mparticle.uc.cn/api/att['v-for'
;……// 递归创建子节点_jr_ydby_new_node_instance(ctx_id v_dom sub_t_node current_node v_f_ctx is_batchCreate);return current_node;
5.在 C++ 环境中构建 Component Tree在构建 V-Dom Tree 的过程中 , 每创建一个节点 , 都会调用 _jr_ydby_add_element 方法 , 向宿主C++环境发起创建节点的指令:
_jr_ydby_add_element(current_node current_node.node_index);// 搜集节点信息var nodePorperty = {template_id: node.template_idctx_d: node.ctx_idtag: node.tagid: node.idis_root: node.is_roottype: node.typeparent_node: node.parent_nodestyle: node.stylecache: node.cacheattr: _jr_ydby_tools_deep_copy(node.attr)value: node.valueevent: node.eventindex: node.index//仅cell-slot节点使用isComponentNode: node.isComponentNodecomponentInstanceId: node.componentInstanceIdcomponentBundleName: node.componentBundleName;// 调用添加方法callAddElement(node.ctx_id node.parent_node nodePorperty index);在 App 启动时 , 已向 JS 虚拟机内植入 callAddElement 方法 , 这里通过 JSI通道将指令从 JS 打通到C++环境
runtime_->global().setProperty( *runtime_ \"callAddElement\"Function::createFromHostFunction( *runtime_PropNameID::forAscii(*runtime_ \"callAddElement\") 4[
(jsi::Runtime &runtimejsi::Value const & /*thisValue*/jsi::Value const *argumentssize_t /*count*/) noexcept -> jsi::Value {UIManager::callAddElement(surfaceIdFromValue(runtime arguments[0
)stringFromValue(runtime arguments[1
)commandArgsFromValue(runtime arguments[2
)arguments[3
.getNumber());return jsi::Value::undefined();));
在C++环境中接收到添加指令后 , 这里开始构建 Component Tree , 保存各节点数据信息 , 并不会同步在界面上添加视图(因为每次使用 N-API在 C++ 和 ArkTS 中通讯 , 都会因跨语言和类型转换带来一定的性能损耗) , 待接收到 V-Dom Tree 构建完成的消息后 , 对应的 Component Tree 的结构也构建完成了 , 这时再一次性完成视图的绘制 。
void UIManager::callAddElement(SurfaceId surfaceId std::string const &parent_id folly::dynamic props size_t index) {ComponentName name = props[\"type\"
.asString();RomaEnv::getInstance().getBackgroundExecutor()([=
() {RomaNode::Shared node = nullptr;auto nonConstRef= RomaInstanceManager::getInstance().get(surfaceId);auto parent = nonConstRef->getNode(parent_id);if(parent == nullptr){return;// 强引用保存节点到父节点的children_数组中folly::dynamic style = props[\"style\"
;node = RomaNodeFactory::createSharedNode(surfaceId tag name parent_id index isComponentNodecomponentInstanceId props[\"attr\"
style props[\"event\"
props[\"value\"
);size_t childIndex = index;parent->appendChild(node childIndex);auto shadowView = std::make_shared<ShadowView>(*node);// 组件节点 , 只插入 , 不创建 , 等组件实例创建时 , 再创建if (!isComponentNode) {// 增加普通节点的创建指令nonConstRef->rootInstance_.lock()->lastMutations_.push_back(ShadowViewMutation::CreateMutation(shadowView->getSharedShadowView()));if ((parent->getComponentName() == \"document\") && nonConstRef->isComponent) {// 如果是子组件中的节点 , 并且是document的直接子节点auto parentNonConstRef = RomaInstanceManager::getInstance().get(nonConstRef->parentInstanceId_);if (parentNonConstRef) {auto componentNode = parentNonConstRef->getNode(nonConstRef->componentNodeId_);if (componentNode) {if (RomaEnv::getInstance().isUseYoga) {// 父子组件衔接yoga树componentNode->appendChild(node index);// 增加组件节点的插入指令auto parentShadowView = componentNode->getShadowView();auto documentShadowView = parent->getShadowView();……for (auto const &pair : documentShadowView->style_.items()) {parentShadowView->style_[pair.first
= pair.second;nonConstRef->rootInstance_.lock()->lastMutations_.push_back(ShadowViewMutation::InsertMutation(parentShadowView shadowView->getSharedShadowView() index));else {// 增加普通节点的插入指令auto parentShadowView = parent->getShadowView();nonConstRef->rootInstance_.lock()->lastMutations_.push_back(ShadowViewMutation::InsertMutation(parentShadowView shadowView->getSharedShadowView() childIndex));// 保存到节点node->setShadowView(shadowView->getSharedShadowView());// 弱引用保存节点到实例的map中nonConstRef->addNode(tag node););
6. 在 JS 环境中发送节点创建完成的消息V-Dom Tree 构建完成后 , 会调用 _jr_ydby_create_finshed
export function _jr_ydby_create_finshed(ctx_id) {let instance = getInstanceById(ctx_id);callCreateFinish(ctx_id{template_id:instance.template_idversion:instance.currentVersion);同样在动态化 SDK 初始化阶段 , 已向 JS虚拟机中植入callCreateFinish方法 , 当 JS 端调用callCreateFinish时 , 通过 JSI 通道将数据传递到 C++环境中 。
runtime_->global().setProperty(*runtime_ \"callCreateFinish\"Function::createFromHostFunction(*runtime_PropNameID::forAscii(*runtime_ \"callCreateFinish\") 1[
(jsi::Runtime &runtime jsi::Value const &jsi::Value const *arguments size_t /*count*/) noexcept -> jsi::Value {auto surfaceId = surfaceIdFromValue(runtime arguments[0
);UIManager::callCreateFinish(surfaceId);return jsi::Value::undefined();));
在C++环境中收到创建完成的消息后 , 执行 yoga 布局 , 获取视图尺寸并调用在 Instance 创建过程中预置的状态监听 mutationsListener 方法 。
void UIManager::callCreateFinish(SurfaceId surfaceId) {RomaEnv::getInstance().getBackgroundExecutor()([=
() {auto nonConstRef = RomaInstanceManager::getInstance().get(surfaceId);if (nonConstRef && !nonConstRef->isComponent) {// 执行yoga布局// Layout nodes.std::vector<YogaLayoutableShadowNode const *> affectedLayoutableNodes{;// affectedLayoutableNodes.reserve(1024);LayoutContext layoutContext = LayoutContext();……if(starts_with(nonConstRef->rootInstance_.lock()->jueName_ \"template\")){layoutContext.layoutType = TEMPLATE;if (nonConstRef->rootInstance_.lock()) {nonConstRef->rootInstance_.lock()->rootNode_->layoutIfNeeded(layoutContext);if (nonConstRef->rootInstance_.lock()) {ShadowViewMutationList mutableList = nonConstRef->rootInstance_.lock()->lastMutations_;nonConstRef->rootInstance_.lock()->lastMutations_.clear();RomaEnv::getInstance().taskExecutor_->runTask(TaskThread::MAIN [nonConstRef mutableList surfaceId
{// 使用 ArkUI 渲染, 并触发 mutationsListener 监听if (nonConstRef->rootInstance_.lock()) {auto a = nonConstRef->rootInstance_.lock()->m_mutationsToNapiConverter;nonConstRef->rootInstance_.lock()->mutationsListener(a mutableListtrue);nonConstRef->createFinish(););else if (nonConstRef) {RomaEnv::getInstance().taskExecutor_->runTask(TaskThread::MAIN [nonConstRef surfaceId
{ nonConstRef->createFinish(); ););
下面我们具体分析一下在触发 mutationsListener 后 , 都发生了什么?通过 C++ 的转发 , 最终在 ArkTS 环境中触发如下回调(这是在 ArkTS创建 Instance 时就通过 N-API传入到 C++ 环境的参数之一 , 上面有介绍)
(mutations: Mutation[
isFromCore: boolean) => { this.descriptorManager.applyMutations(mutations isFromCore)
7.在 ArkTS 环境中构建 Render Tree实例创建完成后触发 applyMutations 方法 , 将各节点数据都保存到 descriptor 中 。
public applyMutations(mutations: Mutation[
isFromCore: boolean) {// 去重const tags = mutations.flatMap(mutation => this.applyMutation(mutation isFromCore));const tags = new Set(tags);// 遍历各节点tags.forEach(tag => {// 取实例id , 和tagconst strArr: string[
= tag.split(\"##\");const instanceId = strArr[0
;//实例idconst nodeId = strArr[1
;//节点idif(instanceId === this.romaInstance.getId()){// 更新节点let updatedDescriptor = this.getDescriptor(nodeId);if(!updatedDescriptor) return;// 在当前实例中更新tag组件的UI描述信息this.descriptorListenersSetByTag.get(nodeId)?.forEach(cb => {onDescriptorChange(cb updatedDescriptor););else {// 创建节点const instance: RomaInstance = RomaEnv.getRomaInstanceManager()?.getInstance(instanceId) as RomaInstance;let updatedDescriptor= instance?.getDescriptor(nodeId);if(!updatedDescriptor) return;instance.refreshComponentUI(nodeId updatedDescriptor););
在 refreshComponentUI 方法中会触发当前节点对应标签的数据变化监听
public refreshComponentUI(tag: Tag d: Descriptor) {this.getDescriptorListenersSet(tag)?.forEach(cb => {onDescriptorChange(cbd););假如标签是 image, 则触发 image 标签的在 aboutToAppear 中的关于标签数据 descriptor 的监听 , 从 newDescriptor 中获取标签上所有的数据 , 包括尺寸数据 。
aboutToAppear() {if (!this.componentCtx) {return;this.componentCtx?.aboutToAppear((newDescriptor) => {this.descriptor = newDescriptor;// 链接自定义alt图方法this.customAltImage = RomaConfig.instance().getImageAltMethod();// 触发更新this.onLoadStart();this.updateImageSource();// 处理图片 object-position 模式相关逻辑this.initObjectPositionHandle()// 解析占位图this.altSource = this.getImageAlt();this.hasPlaceHoldImage = this.altSource ? true : false;// 解析背景色let bgColorStr = RomaStyleParser.getStyleToString('background-color'this.descriptor);……// 设置tint-colorthis.getTintColor();// 是否开启抗锯齿this.interpolation = this.colorFilter ? ImageInterpolation.High : ImageInterpolation.Low;(methodName args:TAny[
) => {// 注册标签方法if (methodName === 'loadRef') {this.loadRef(args[0
););
8.根据 Render Tree 绘制视图到此我们也仅仅是完成了进入动态化页面 RomaInstanceView 视图中的 abountToAppear 方法中的数据准备工作 。 完成了三个 Instance 的创建 , 完成了三棵树的创建以及把所有组件和标签相关数据都保存到对应的 descriptor 中 , 接下来就是在 build 方法中真正绘制视图了 。
public build() {// 根据表述信息 , 构建RomaComponentFactory.builder(new RomaComponentParam(this.romaInstance this.descriptor.tag));RomaComponentFactory是所有标签组件的工厂方法 , 定义如下:
export const RomaComponentFactory: WrappedBuilder<[RomaComponentParam
> = wrapBuilder(RomaComponentFactoryBuilder);
在调用 RomaComponentFactory.builder 时 , 触发 RomaComponentFactoryBuilder 方法 , 如下只列出示例中用到的标签的实现 , 其他的省略了 。
param.descriptor 中保存了已经构建好的 Render Tree , 包括各节点对应的标签类型和所有的节点数据 。
@Builderfunction RomaComponentFactoryBuilder(param: RomaComponentParam) {if (param.type == \"document\") {RomaDocument({componentCtx: param.componentCtx)else if (param.type == \"div\") {RomaDiv({componentCtx: param.componentCtx)else if (param.type === \"text\") {RomaText({componentCtx: param.componentCtx)else if (param.type === \"image\" || param.type === \"img\") {RomaImageView({componentCtx: param.componentCtx)else {RomaCustomComponentFactory.customComponentBuilder.builder(param.componentCtx);以图片标签为例 , 会创建 RomaImageView 对象 , 调用其 build 方法 , 最终将 image 视图渲染到页面上 。
build() {if(this.componentCtx && this.descriptor) {Image(this.imgSource).attributeModifier(this.componentCtx?.build(this.descriptor)).gestureModifier(this.componentCtx?.build(this.descriptor)).alt(this.getImageAlt()).objectFit(this.getResizeMode(RomaStyleParser.getStyleToString('object-fit' this.descriptor))).renderMode(this.getRenderMode()).colorFilter(this.colorFilter).interpolation(this.interpolation).backgroundColor(this.showColor).blur(this.getBlurNumber()).onComplete(event => this.onLoad(event as ImageOnCompleteEvent)).onError(event => this.dispatchOnError(event as ImageOnErrorEvent)).position(this.imgPosition).clipShape(this.imgClipShape)至此!动态化终不辱使命 , 一路逢山开路 , 遇水架桥 , 跨越层层关隘 , 破天命所缚 , 所言终达天听 , 辅社稷 , 开盛世太平!就是说动态化完成了从资源被加载-到数据被层层加工流转-到视图被创建-再到渲染到页面上的全过程 。
五、视图更新流程当点击“更新节点数据”按钮后 , 图片资源被修改 , div视图的背景色也被修改 。 相比创建的过程 , 这个页面中节点数据的更新要简单一些 , 因为各环境下的 Instance 都已经创建好了 , 各环境中的通道也打通了 , 只是各节点Differ(数据对比)的过程 , 并将 Differ 结果通过各环境中的通道传递给相关视图节点更新即可 。 这里不详细介绍具体的过程了 , 因为数据在三种语言环境中传递的逻辑是一致的 , 这里只介绍一下 Differ 的逻辑 。
1.Differ 原理介绍当业务需要更新视图的时候 , 会根据新的视图数据重新生成一棵新的 V-Dom Tree , 和页面旧的 V-Dom Tree 进行对比 , 最终得到需要更新的节点数组 , 将这个数组同步到 ArkTS 的 Render Tree 的对应的节点 , 触发相应节点更新 。



??


六、规划总结目前已使用三个线程确保数据在不同环境中的高效处理 , 为了避免阻塞 JS 线程和 UI 线程 , 已将复杂耗时的功能放到 bg 线程中处理 , 尽可能的提升了页面绘制效率 。 但使用 ArkUI 封装好的视图组件绘制的视图层级相比 Android 和 iOS端多出一倍 , 且使用N-API通讯会带来一定的性能开销 , 因此先天性的多做了很多的工作 。 为了进一步提升视图渲染和数据通讯的效率 , 计划接下来将 C-API (鸿蒙提供的一组绘制视图的 C 接口)接入到动态化鸿蒙 SDK 中 , 在 C++ 环境中就完成视图的绘制 , 以更直接和高效的方式绘制视图 , 视图的层级将减少一半 , 同时省去了跨语言通讯的相关成本 。 经测试 , 视图绘制和渲染效率将进一步提升 , 用户将获得更好的使用体验 。
【鸿蒙跨端实践-揭秘视图渲染流程】动态化是一个涉及 Android、iOS、Harmony、Web、Java、C/C++、Vue、JavaScript、Node、Webpack、CLang、Ninja 等众多领域的综合解决方案 。 我们有各个领域优秀的小伙伴共同前行 , 如果你想深入了解某个领域的具体实现 , 可在评论区留言随时交流~!

    推荐阅读