作者:京东科技 胡大海
前言动态化跨端框架(后文统称“动态化”)是一个由京东金融大前端团队全自主研发的,一份代码,可以在HarmonyOS、iOS、Android、Web四端运行的跨平台解决方案。在研发团队使用后可大幅降低研发人力成本;为业务提供实时触达、A/B触达等能力以提升业务投放效率;同时保障了C端用户优秀的用户体验。
一、动态化跨端框架原理介绍通过上图,我们先了解一下动态化跨端框架在iOS、Android等多个平台实现跨端的原理:
① 业务层:业务代码经过打包后形成business.js发布到云端,被Harmony、iOS、Android、H5四端共用。
② 虚拟机层:虚拟机的核心职责是运行js代码,这也是跨平台框架的基础,在iOS使用系统内置的JSCore,在Android使用V8,在浏览器使用Webkit,在鸿蒙系统我们依然需要一个能够运行js代码的虚拟机。
③ 通讯层:在iOS和Android端使用了json数据格式进行通讯数据的传输,在鸿蒙系统也可以使用该方案。
④ 渲染层:使用各个系统的系统组件进行UI元素的渲染。
二、基于方舟虚拟机的方案探索1、方舟字节码概念方舟字节码(Ark Bytecode),是由方舟编译器编译ArkTS/TS/JS生成的,提供给方舟运行时解释执行的二进制文件,字节码中的主要内容是方舟字节码指令。
2、在方舟虚拟机中运行JS方舟虚拟机不能直接运行当前在V8中运行的js代码,但是能够执行方舟字节码,所以我们可以借助鸿蒙提供的工具将js代码转化为方舟字节码,这样就能利用鸿蒙系统的方舟虚拟机执行我们的js代码了。
3、存在的问题3.1、业务无法热更新在iOS和Android端,业务可以随时打包后在云端发布新的版本,借助于JSCore或者V8就可以直接运行新的版本的js,这样就支持了业务的热更新发布。但在鸿蒙系统上,华为基于安全考虑,business.abc这样的字节码文件不支持动态下发,需要内置到APP中,这样就失去了业务热更新的能力。
3.2、单线程性能问题在其他两端我们是开启了一个单独的JS线程,进行business.js文件的执行,但是如果我们使用方舟虚拟机执行business.js转换来的business.abc的时候,其实是在方舟虚拟机的UI主线程运行了这个文件。在其他两端js文件在JS线程执行的时候,UI渲染和交互是并行不受影响的,但是在方舟虚拟机单线程下abc文件的执行和UI渲染&交互变成了串行,这样必然会严重影响页面渲染速度和交互的流畅度。
业务不能热更新以及单线程性能不佳等问题的存在,我们决定使用另一种方案-V8虚拟机。
三、基于V8虚拟机的方案落地1、在V8虚拟机中运行JS如果能把V8移植到鸿蒙系统中,我们就可以像其他两端一样使用多线程并且能实现业务热更新等特性,但是V8是一个近千万级代码的庞大仓库,需要掌握CMake、Clang、LLVM、Ninja等一系列交叉编译知识(嵌入式范畴),对于应用开发者是一个巨大的挑战,虽然我们已经掌握了V8移植到鸿蒙的技术,但从包大小、稳定性、兼容性、维护成本等维度看,华为厂商内置V8是一个具有长期收益的重大事项,通过和华为持续沟通,最终华为将V8内置到了操作系统,业界所有类动态化框均可直接使用内置V8虚拟机进行跨端框架的适配。
2、高性能核心方案2.1、多线程架构多线程是提高程序性能最直接、最有效的手段之一,借助于鸿蒙系统内置的V8虚拟机,我们就能像iOS、Android两端一样使用三线程模型完成动态化跨端框架在鸿蒙系统的渲染过程。
JS线程负责将业务代码解析为一颗虚拟Dom树、发出渲染命令、处理业务逻辑等,通过接口定义的桥方法发送给组件线程进行处理。我们以添加一个点击按钮节点为例,JS线程会通过“添加节点”这个接口以JSON描述的方式,将信息传递给组件线程,组件线程根据JSON描述将这个点击按钮节点添加到组件树中,然后触发UI线程创建系统组件,比如在鸿蒙系统会创建一个ArkTS的按钮组件,在iOS系统会创建一个UIButton组件。
UI线程负责用户页面滑动、点击事件等交互行为,当发生比如用户点击事件后,同样通过接口定义的桥方法“调用JS”,将点击事件传递给JS线程进行处理,紧接着继续处理UI线程的任务,这样UI线程的交互效率就高了,充分保障了用户良好的操作体验。
//JSON描述示例{ "type":"btn", "value":"按钮", "childrens":[], "id":"238346e885ee", "style":{ "width":"66px" }, "attr":{ "text-color":"#FFFFFFFF" }, "event":{ "onclick":"myclick()" }}2.2、JSI技术引入通讯桥存在的问题动态化基于三个线程并行运行的方式,使其渲染性能已经接近于原生的渲染性能,但是在一些频繁通讯场景,通讯桥会“堵塞”,比如当业务需要监听一个页面的滑动而改变另外一个元素背景色的透明度,那么JS线程大部分时间在处理接收列表滑动距离,改变元素背景色透明度这个任务中,其他任务的执行会被严重影响。另外JSON数据传输的序列化和反序列化过程也会带来很大的线程性能损耗。
解决方案-JSI之前使用通讯桥的一个主要原因就是 C++ 中的函数没办法完整映射到 JavaScript 中,让 JavaScript 直接调用,所以只能选择以序列化字符串的形式通过通讯桥传输。而JSI做的事情就是将 C++ 中的常用类型(函数、对象等)一一映射到 JavaScript 中,我们就能在JS中直接调用C++的函数和对象了。因为消除了桥通讯带来的序列化和异步调用的开销,大大提升了线程通讯性能。
四、进一步优化的方向1、减少UI层级当前基于多线程和JSI的架构模式在鸿蒙系统的性能还算不差,但是在鸿蒙系统上同样一个业务的UI层级是其他两端层级的约2倍。原因在于在鸿蒙系统使用系统组件进行递归渲染的时候,需要借助自定义组件进行实现,然而和iOS和Android端的命令式组件渲染不同,比如RomaDiv对应iOS就是直接翻译为UIView即可,在鸿蒙必须增加一个包裹的容器才是一个合法的自定义组件,比如Stack容器,这样每个组件的层级就多了一层,层级过多会直接影响渲染性能,在一些复杂业务场景到达一定层级后会造成页面掉帧和卡顿。
@Componentexport struct RomaDiv { build() { Stack(){ //借助wrapBuilder实现递归 ForEach(this.childrenTags, (childrenTag) => { RomaComponentFactory.builder()//RomaComponentFactory就是对应鸿蒙系统提供的WrappedBuilder }) } } }面对业界跨端框架面临的这个共性问题,鸿蒙系统提供了C语言的命令式接口进行组件创建,C组件接口是介于UI组件的Native实现和ArkTS对接层之间的一层C接口封装,它绕过了状态管理对组件变化、刷新的自动化管理,因此具有较好的性能。同时经过初步验证,接入C-API后,UI层级能直接和另外两端保持一致,同时渲染性能也会得到大幅提升。
2、降低通讯成本当前JSI在鸿蒙系统的应用中通过JSI打通C++,再通过NAPI从C++打通ArkTS,跨语言通讯成本高。如果接入了C-API,就避免了C++和ArkTS之间类型互相转换和跨语言调用的开销,也能带来不少的性能提升。
3、JS逻辑下沉到C++在当前架构中,JS线程运行着V-Dom树创建、对比,样式&属性解析等一系列繁重的框架逻辑,如果我们能将这些JS代码逻辑下沉到C++,框架逻辑运行效率会进一步提升。
总结本文讲述了如何在鸿蒙系统中实现“动态化”跨端框架的高性能运行。包含探索方舟虚拟机运行方案时遇到的问题,以及基于V8虚拟机方案的具体提升手段和后续进一步提升的方案。通过阅读,你将能够更好地理解和应用这些技术,提高跨端框架的性能,提升C端用户体验。
PS: 如果你在实践中遇到任何问题或有新的见解,欢迎在评论区中与我分享。期待与你交流!