写点什么

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

  • 2024-09-30
    北京
  • 本文字数:4568 字

    阅读完需:约 15 分钟

作者:京东科技 杜强强

前言

在 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 作为通信层完成设计。



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、未来技术做好了基础,旨在带来更优的性能和更好的用户体验。

发布于: 刚刚阅读数: 5
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
鸿蒙跨端实践-JS虚拟机架构实现_京东科技开发者_InfoQ写作社区