作者:京东科技 杜强强
前言
在Roma跨端方案中,JS虚拟机是框架的焦点,负责实验动态化的JS代码。在Android平台采用了基于V8的J2V8,iOS平台则利用了系统自带的JSCore,而在HarmonyOS中,由于业界无类似的框架,我们需要自行实现以确保焦点基础能力的完整。 鸿蒙虚拟机的开发经历了从最初 ArkTs2V8 到JSVM + Roma新架构方案。在此过程中,我们实现了完整的鸿蒙版的“J2V8”和 基于系统JSVM的JS虚拟机框架,解决了JS引擎库移植、多语言通信能力、多范例数据结构转换等浩繁挑战。本文将从实现的各个阶段过程出发,探究在实践中碰到的题目及解决方案。
一、鸿蒙版 “J2V8”虚拟机实现 - ArkTs2V8
ArkTs2V8框架依赖 V8引擎, 鸿蒙前期交叉编译资料少,V8官方也未有HarmonyOS端编译方式。因此在这过程中, 我们采取初期利用QuickJS引擎(C语言开发,代码少,移植方便), 后期自编译V8完成后替换QuickJS, 保证快速验证跨端前期技能调研方案以及其他依赖项基础能力的开展。 自编译V8 通过学习交叉编译相干技能,探索式逐步解决编译期间这种报错,完成V8虚拟机移植。
ArkTs2V8 架构鉴戒了Android J2V8( 动态化-J2V8文章中报告了具体原理及实践)的实现原理。 J2V8为针对V8的 Java实现,采用最直接的方式在Java中访问V8原始值,因此具备较高的性能。 在HarmonyOS中,采用V8作为JS引擎, JSI作为通信层完成设计。
1、引入JSI
考虑到跨端框架的将来发展,固然通过C++ 可以或许直接与V8交互,但这种方式不利于虚拟机代码的共享和扩展。因此Roma框架引入JSI,以增强代码的可扩展性,促进更有效的代码共享,并实现更灵活的虚拟机集成。
JSI(JavaScript Interface),轻量级,通用且同步的JavaScript接口, 通过JSI,JS代码可以直接与C++原生代码通信。
有了JSI层对虚拟机的封装,Roma框架开发者无需在关心虚拟机底层能力, 同时也可以自由切换引擎,好比利用V8,QuickJS、JSVM等, 规范了数据格式,统一为JSIValue。
2、API与框架设计原理
接口设计采用和J2V8 类似的设计,支持多虚拟机实例方式。
实现原理:
1、本地接口: 利用 napi 利用创建桥梁, 完本钱地代码调用Quick引擎函数。
2、C++数据绑定:在C++层面 ,界说虚拟机交互操纵的相干函数,完成V8引擎相干API 来实验JS代码、 处理惩罚JS对象和实验虚拟机相干的操纵。
3、JSIRuntime: 在C++层面引入JSI概念,通过完成JSIRuntime - QuickJSRuntime & V8Runtime, 完成虚拟机层通信能力。
4、虚拟机对象的界说及封装:根据JS数据范例,界说ArkTS数据结构,包括基本数据范例、JSObject、JSArray、JSFunction。ArkTS侧 范例对象持有C++ JSIValue 对象指针,当实验具体能力时,通过napi 传递指针,完成具体功能的调用。 简朴来说,相当于ArkTS JS对象署理C++ 虚拟机数据对象。
5、内存管理: ArkTs2V8负责管理ArkTS与JSValue 之间的内存交互。此中C++侧完成JSValue对象的创建、引用持有与烧毁。 ArkTS数据对象中界说对象开释函数, 数据利用完后,由ArkTS调用开释内存。
ArkTs2V8架构设计支持虚拟机多实例, 单个虚拟机的创建过程时由 ArkTs通过JSEngine发起创建JSRuntime虚拟机实例创建,经过napi,在C++环境创建JSRuntime引擎实例及引用, 并完成环境Context及global的初始化, 同时创建ArkTs JSRuntime对象,署理C++虚拟机对象JSRuntime(QuickJSRuntime or V8Runtime) 并绑定指针引用。
初始化过程:
V8Runtime实现
3、JS、JSI、JSRuntime 关系
JSRuntime (QuickJSRuntime or V8Runtime) 是 JS运行时环境。一个 JSRuntime 通常包括一个或多个引擎,JSI 可以看作是毗连 JS 代码和 JSRuntime 的桥梁。通过 JSI,开发者可以更直接地与 JSRuntime 交互,实现原生功能的调用和管理。
4、部门过程剖析
ArkTs2V8实现的过程中,最基础的两个功能原理:JSObject对象的创建与获取、原生方法的注入, 这两个能力的实现可以扩展到其他大多数API功能实现上。
1、JSObject对象及获取对象数据过程。
通过JSRuntime 发起接口的调用,通过napi,根据对象范例在C++侧创建对象的JSValue对象及象指针引用, 并将引用指针绑定至ArkTS对象,完成对象的创建。
2、 JS虚拟机注入原生方法
ArkTS方法到JS虚拟机中,主要实现原理:
将ArkTs的方法 和 目标注册对象指针 天生MethodDescriptor方法描述对象, 通过functionID将对象存储在当前JSContext环境中。 通过napi 发起在C++侧署理函数HostFunction的创建,并绑定ArkTs的方法的引用。 进入到JSI内部,创建方法署理HostFunctionProxy 对象,绑定署理方法HostFunction及守护函数Finalizer, v8::External 将HostFunctionProxy与 JS环境对象(V8对象) 关联起来,天生V8 Function , 此时V8函数会与HostFunctionProxy生命周期绑定。 简朴来说相当于ArkTS callback,传递至C++,C++创建JSI Callback并绑定ArkTS callback, JSI Callback 设置到HostFunctionProxy中,HostFunctionProxy 通过 v8::External与 JS环境绑定。
当JS触发该该函数时,通过v8::External绑定HostFunctionProxy这层关系,HostFunctionProxy中JSI Callback会收到JS环境的响应消息,在通过绑定的ArkTs的方法 通过napi接口返回至ArkTS中,最终ArkTS收到方法响应。
这种署理函数的实现, 初次学习大概比力复杂,但整个过程现实是多个对象间引用的持久化和差别数据对象的交换, 大致过程图如下:
4、题目及挑战
1、 数据对象的内存管理
手动内存管理。 ArkTs2V8 负责管理ArkTS与V8之间的内存交互中,ArkTs发起对象的创建和烧毁。 整个内存的管理是基于手动管理,需利用方用完后及时关闭,避免内存泄漏。 这种设计模式下,利用者操纵不当极为容易造成内存泄漏,并且利用也较为不便。
针对这题目,在后续的迭代设计中,将内存管理升级为自动内存管理的方式。 JS为单线程实验,单方法片断或一些逻辑中,如果有了调用开始时机和结束调用时机, 通过开始时记载当前时刻后开始创建的对象,在调用结束时刻对记载的对象举行统一的内存开释,类似于标记垃圾回收,完成内存的统一管理。借助Roma框架中对虚拟机层的封装,做到了内存自动管理。
2、 跨语言性能题目
基于ArkTs2V8 的API实现,在原生、JS环境中,无法直接利用对方的数据范例,二者之间数据范例需要转换。 JS到原生的过程中,ArkTs2V8中目前提供的API仅可以获取当前层级的JS对象数据,子对象数据需要通过递归遍历从JS环境中逐一获取。因此解析的过程中需要频繁的通过C++读取V8,当数据量较大时通常比力耗时。拿常用的网络模块来说,接口下发的业务接口数据至少都在几K乃至几十K,转为JS对象在中端性能手机上 会有几十ms的耗时,这对单线程模式的JS环境来说影响时巨大的。
ArkTS 、C++ 跨语言通信性能我们可以采取类似于Roma Android的通信次数压缩策略,或者利用JSON序列化来减少跨语言交互的性能消耗, 但无论用哪种都仅是从行为上规避跨语言的性能,而无法彻底解决。
3、 线程管理题目
ArkTS基于TS语言,由于语言特性,ArkTS线程隔离,那么对于ArkTs2V8这种接口设计并不友好。 JS线程需要在ArkTS开启独立worker JS线程,收发JS消息,线程间的隔离,涉及再次序列化数据影响性能。
基于题目2、3 以及对框架将来的思索,Roma鸿蒙端决定采用新的方案: 框架C++化,框架逻辑实现全部放在native侧, 虚拟机实现全部切C++,C++侧完成线程管理,ArkTS不在负担线程和逻辑任务。 这种既提拔了解决了题目,提拔框架性能,也为以后框架移植其他平台打好基础 。
二、基于JSVM虚拟机实现 (Roma新架构)
1、鸿蒙JSVM
在V8移植上,从短期看固然我们初步把握了V8交叉编译移植技能,但从稳固性、兼容性、维护本钱、包巨细等维度看, 采用系统内置虚拟机有巨大的长期收益。 年初Roma框架与华为专家多次沟通交换,最终HarmonyOS将V8内置到了操纵系统, Q2我们实现了第三个JSRuntime - JSMVRuntime, 至此鸿蒙动态化架构修改趋于稳固。
JSVMRuntime:
2、新架构思路 - Roma架构C++化
新架构的设计思路SDK焦点逻辑整体C++侧实现, 如许在底层引擎与焦点流程之间可以直接c++通信,线程间上与其他端保持相同 - 三线程模子JS线程 +UI线程 + 耗时计算线程。通过C++ PThread完成线程管理, 从而避免跨语言、ArkTS线程隔离带来的多种性能消耗。 在数据结构设计上, JS数据采用JSI::Value, 与其他线程数据相互交互时, 统一利用folly完成。 别的将虚拟机层下沉,对外提供JSExecutor, 功能开发时框架开发者无需关心虚拟机层的实现。
虚拟机方法与对象的注入上, 通过HostObject署理对象能力的双边映射,原生模块直接与JS 同步或异步交互, 从而缩短了流程链路。
框架大致原理:
3、过程碰到的
1、JSVM字符串引用题目
JSVMRuntime实现期间,字符串无法创建对象引用。 JSI的设计中将字符串作为 pointer 自界说指针类,通过指针地点访问, 与其相同的还有对象,方法。 在很多语言中字符串都作为一种特别的范例(非基本数据范例), 比方在C++中,字符是一种基本数据范例,但是字符串不是,字符串由字符组成, V8引擎亦如此。 V8中通常利用v8::String来创建JS字符串, 我们可以对齐举行持久化引用。
而JSVM中 OH_JSVM_CreateReference 无法针对字符串范例创建引用, 字符串的持久化需从JSVM_Value从copy出来通过智能指针或者new内存的方式举行存储,这种copy持久化的方式会造成字符串内存两份(JSVM一份,自己存一份), 现实开发中大量的字符串范例转换,如许会造成内存占比过高。
为此, 经过与华为专家多次交换沟通,最终将字符串归为引用范例,可通过OH_JSVM_CreateReference持久化引用,修改后的方式如下:
2、HostObject署理对象实现
HostObject 是JS对象,提供与原生直接通信的方式。 相当于 native 在 JS的署理对象,双向映射,原生模块直接与JS 同步或异步交互, 在一些功能实现上可以缩短流程链路, 在JS中可直接调用C++的对象。 在动态化中, 模块的实现采用的就是HostObject能力, 框架层实现模块署理对象及桥通信层面的双向通信过程。 好比登录模块,在ArkTS侧封装模块的API,通过C侧的HostObject映射,可以在JS中直接调用登录模块的登录,退出登录等能力。 HostObject的实现,固然在框架层面相比于乔通道的方式更加复杂,但对于复杂逻辑流程和交互链路, 基础开发可以更注重于功能逻辑。
HostObject 实现过程较为复杂, 但我们可以将过程拆分,通过对象管理 + 署理函数的方式将过程简化。 首先对象的管理直接JSRuntime中持久化即可,Roma中采用智能指针,那么就剩下署理函数,前面我们讲了JS中注入方法里面包括了署理函数的实现原理,采用类似的思路来完成HostObject。
HarmonyOS提供的JSVM API最初仅支持署理函数的创建, 而我们需要是创建署理对象,对象中可以有恣意方法,仅通过署理函数方式无法满足恣意方法的需求,为此通过在JS中注入署理对象脚本实现,通过Proxy署理的方式,将get、set等署理对象的方法通过署理函数的方式返回,这种情况下,我们的函数数量就被简化成了get、set及一些固定的方法。 通过这些方法做署理转接,调用到C++对象方法,借助JSI::Value的包装,将具体效果返回。
JS署理脚本部门代码:
大致实现过程:
示例 - 基于HostObject Console能力实现
三、总结
0到1实现鸿蒙版“j2v8”、“JSRuntime” 让我们更加了解引擎实现中的各种细节和一些难点题目的解决。 一些方案的实现,也可以延展到其他(非虚拟机)场景。 Roma 框架C++, 让Roma框架走向技能深水区, 为以后capi、将来技能做好了基础,旨在带来更优的性能和更好的用户体验。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |