鸿蒙跨端实践-JS虚拟机架构实现

京东云开发者
• 阅读 282

作者:京东科技 杜强强

前言

在Roma跨端方案中,JS虚拟机是框架的核心,负责执行动态化的JS代码。在Android平台采用了基于V8的J2V8,iOS平台则使用了系统自带的JSCore,而在HarmonyOS中,由于业界无类似的框架,我们需要自行实现以确保核心基础能力的完整。 鸿蒙虚拟机的开发经历了从最初 ArkTs2V8JSVM + 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作为通信层完成设计。

 鸿蒙跨端实践-JS虚拟机架构实现

1、引入JSI

考虑到跨端框架的未来发展,虽然通过C++ 能够直接与V8交互,但这种方式不利于虚拟机代码的共享和扩展。因此Roma框架引入JSI,以增强代码的可扩展性,促进更有效的代码共享,并实现更灵活的虚拟机集成。

JSI(JavaScript Interface),轻量级,通用且同步的JavaScript接口, 通过JSI,JS代码可以直接与C++原生代码通信。

有了JSI层对虚拟机的封装,Roma框架开发者无需在关心虚拟机底层能力, 同时也可以自由切换引擎,比如使用V8,QuickJS、JSVM等, 规范了数据格式,统一为JSIValue。

2、API与框架设计原理

接口设计采用和J2V8 类似的设计,支持多虚拟机实例方式。

鸿蒙跨端实践-JS虚拟机架构实现 

实现原理:

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) 并绑定指针引用。

初始化过程:

 鸿蒙跨端实践-JS虚拟机架构实现 

V8Runtime实现

 鸿蒙跨端实践-JS虚拟机架构实现



3、JS、JSI、JSRuntime 关系

 鸿蒙跨端实践-JS虚拟机架构实现

JSRuntime (QuickJSRuntime or V8Runtime) 是 JS运行时环境。一个 JSRuntime 通常包括一个或多个引擎,JSI 可以看作是连接 JS 代码和 JSRuntime 的桥梁。通过 JSI,开发者可以更直接地与 JSRuntime 交互,实现原生功能的调用和管理。

4、部分过程剖析

ArkTs2V8实现的过程中,最基础的两个功能原理:JSObject对象的创建与获取、原生方法的注入, 这两个能力的实现可以扩展到其他大多数API功能实现上。

1、JSObject对象及获取对象数据过程。

通过JSRuntime 发起接口的调用,通过napi,根据对象类型在C++侧创建对象的JSValue对象及象指针引用, 并将引用指针绑定至ArkTS对象,完成对象的创建。



鸿蒙跨端实践-JS虚拟机架构实现



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收到方法响应。

这种代理函数的实现, 初次学习可能比较复杂,但整个过程实际是多个对象间引用的持久化和不同数据对象的交换, 大致过程图如下:

鸿蒙跨端实践-JS虚拟机架构实现 

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:



鸿蒙跨端实践-JS虚拟机架构实现 

2、新架构思路 - Roma架构C++化

新架构的设计思路SDK核心逻辑整体C++侧实现, 这样在底层引擎与核心流程之间可以直接c++通信,线程间上与其他端保持相同 - 三线程模型JS线程 +UI线程 + 耗时计算线程。通过C++ PThread完成线程管理, 从而避免跨语言、ArkTS线程隔离带来的多种性能损耗。 在数据结构设计上, JS数据采用JSI::Value, 与其他线程数据相互交互时, 统一使用folly完成。 另外将虚拟机层下沉,对外提供JSExecutor, 功能开发时框架开发者无需关心虚拟机层的实现。

虚拟机方法与对象的注入上, 通过HostObject代理对象能力的双边映射,原生模块直接与JS 同步或异步交互, 从而缩短了流程链路。

鸿蒙跨端实践-JS虚拟机架构实现

框架大致原理:



鸿蒙跨端实践-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持久化引用,修改后的方式如下:

鸿蒙跨端实践-JS虚拟机架构实现 

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代理脚本部分代码:

鸿蒙跨端实践-JS虚拟机架构实现

大致实现过程:

鸿蒙跨端实践-JS虚拟机架构实现 

示例 - 基于HostObject Console能力实现

鸿蒙跨端实践-JS虚拟机架构实现 

三、总结

0到1实现鸿蒙版“j2v8”、“JSRuntime” 让我们更加了解引擎实现中的各种细节和一些难点问题的解决。 一些方案的实现,也可以延展到其他(非虚拟机)场景。 Roma 框架C++, 让Roma框架走向技术深水区, 为今后capi、未来技术做好了基础,旨在带来更优的性能和更好的用户体验。

点赞
收藏
评论区
推荐文章
Stella981 Stella981
3年前
Android自动化测试,5个必备的测试框架
AppiumAppium是一个开源的移动测试工具,支持iOS和Android,它可以用来测试任何类型的移动应用(原生、网络和混合)。作为一个跨平台的工具,你可以在不同的平台上运行相同的测试。为了实现跨平台的功能,Appium使用了供应商提供的AndroidUI框架来实施测试:在iOS中使用XCTest,在Android中使用UIAutom
Stella981 Stella981
3年前
JavaScript 如何打败众语言,成为 Node 的实现语言?
Node.js是一种将JS放在服务器端实现的平台,而为什么要把js放到服务器端实现,JS 最初设计是在浏览器端解释执行,后来为什么搞Node.js把他放在服务器端执行呢?关于这个问题,近日有网友解释道:Node.js的作者 RyanDahl是一名资深的C/C程序员,在创造出Node之前,他的主要工作都是围绕高
京东云开发者 京东云开发者
2个月前
鸿蒙跨端实践-ArkTS和CAPI的混合开发实现
一、背景在文章中,讲述了动态化适配鸿蒙的方案实现,当在鸿蒙系统进行UI渲染的时候,我们使用了系统的组件进行递归渲染。在iOS和Android也是借助各自系统组件进行的渲染,但是在鸿蒙系统会存在以下4个严重问题:1.UI层级过多以金融APP理财频道页中的一个
京东云开发者 京东云开发者
2个月前
史无前例,移植V8虚拟机到纯血鸿蒙系统
作者:京东科技于飞跃一、背景\\\\如图所示,Roma框架是我们自主研发的动态化跨平台解决方案,已支持iOS,android,web三端。目前在京东金融APP已经有200页面,200乐高楼层使用,为保证基于Roma框架开发的业务可以零成本、无缝运行到鸿
京东云开发者 京东云开发者
1个月前
揭秘动态化跨端框架在鸿蒙系统下的高性能解决方案
作者:京东科技胡大海前言(后文统称“动态化”)是一个由京东金融大前端团队全自主研发的,一份代码,可以在HarmonyOS、iOS、Android、Web四端运行的跨平台解决方案。在研发团队使用后可大幅降低研发人力成本;为业务提供实时触达、A/B触达等能力以
京东云开发者 京东云开发者
1个月前
鸿蒙跨端实践-揭秘视图渲染流程
作者:京东科技刘宁一、前言通过本文你将全面清晰的洞悉动态化跨端的实现原理,感受黑悟空(数据)一路打怪升级(在不同语言环境中流转改造),逆天改命(操控原生视图绘制),终成齐天大圣(完成视图渲染呈现)的艰辛历程。二、原理介绍1.动态化跨端原理介绍动态化罗码(R
京东云开发者 京东云开发者
2星期前
Taro 鸿蒙技术内幕系列(二):如何让 W3C 标准的 CSS跑在鸿蒙上
作者:京东零售马银涛基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景HarmonyOS采用自研的ArkUI框架作为原生UI开发方案,这套方案有完善的布局系统和样式
京东云开发者 京东云开发者
1星期前
简述大前端技术栈的渲染原理
作者:京东物流卢旭大前端包括哪些技术栈大前端指的是涵盖所有与前端开发相关的技术和平台,应用于各类设备和操作系统上。大前端不仅包括Web开发,还包括移动端开发和跨平台应用开发,具体包括:•原生应用开发:Android、iOS、鸿蒙(HarmonyOS)等;•