在一般的移动端开发场景中,每次更新应用功能都是通过 Native 语言开发并通过应用市场版本分发来实现的。

但是市场瞬息万变,Native 语言在开发效率上存在一定不足,并且从 APP 版本更新 到 应用市场审核发布 再到 用户下载更新,总会存在一定的时间差,这样就导致新的功能无法及时覆盖全量用户。

为了解决这个问题,开发者们一般会在项目里引入一门脚本语言,提速 APP 的研发流程。

在移动端应用比较广泛的脚本语言有 Lua 和 JavaScript,前者在游戏领域用的比较多,后者在应用领域用的比较多。本篇文章主要是想探讨一下移动双端(iOS & Android)的 JavaScript 引擎选型。由于个人水平有限,文章总会有遗漏和不足的地方,还请各位大佬多多指教。

JS 引擎选型要点

JavaScript 作为世界上最热门的脚本语言,有着非常多的引擎实现:有 Apple 御用的 JavaScriptCore,有性能最强劲的 V8,还有最近热度很高的 QuickJS......如何从这些 JS 引擎里选出最适合的?我个人认为要有几个考量:

  • 性能:这个没话说,肯定是越快越好

  • 体积:JS 引擎会增加一定的包体积

  • 内存占用:内存占用越少越好

  • JavaScript 语法支持程度:支持的新语法越多越好

  • 调试的便捷性:是否直接支持 debug?还是需要自己编译实现调试工具链

  • 应用市场平台规范:主要是 iOS 平台,平台禁止应用集成带 JIT 功能的虚拟机

比较麻烦的是,上面的几个点都不是互相独立的:

比如说开启 JIT 的 V8 引擎,性能肯定是最好的,但它引擎体积就很大,内存占用也很高;在包体积上很占优势的 QuickJS,由于没有 JIT 加持,和有 JIT 的引擎比起来平均会有 5-10 倍的性能差距。

下面我会综合刚刚提到的几个点,并选择了 JavaScriptCore,V8,Hermes 和 QuickJS 这 4 个 JSVM,说说它们的优点和特点,再谈谈他们的不足。

JS 引擎功能大比拼

1.JavaScriptCore

mobile_JSVM_JSC

JavaScriptCore 是 WebKit 默认的内嵌 JS 引擎,wikipedia 上都没有独立的词条,只在 WebKit 词条的三级目录[1]里介绍了一下,个人感觉还是有些不像话,毕竟也是老牌 JS 引擎了。

由于 WebKit 是 Apple 率先开源的,所以 WebKit 引擎运用在 Apple 自家的 Safari 浏览器和 WebView 上,尤其是 iOS 系统上,因为 Apple 的限制,所有的网页只能用 WebKit 加载,所以 WebKit 在 iOS 上达到了事实垄断,作为 WebKit 模块一部分的 JSC,顺着政策春风,也「基本」垄断了 iOS 平台的 JS 引擎份额。

垄断归垄断,其实 JSC 的性能还是可以的。

很多人不知道 JSC 的 JIT 功能其实比 V8 还要早,放在十几年前是最好的 JS 引擎,只不过后来被 V8 追了上来。而且 JSC 有个重大利好,在 iOS7 之后,JSC 作为一个系统级的 Framework 开放给开发者使用,也就是说,如果你的 APP 使用 JSC,只需要在项目里 import 一下,包体积是 0 开销的!这点在今天讨论的 JS 引擎中,JSC 是最能打的。

虽然开启 JIT 的 JSC 性能很好,但是只限于苹果御用的 Safari 浏览器和 WKWebView,只有这两个地方 JIT 功能才是默认开启的,如果在项目里直接引入 JSC,JIT 功能是关闭的。为什么这么做呢?RednaxelaFX 大佬[2] 给出过非常专业的解释[3]:

JIT 编译需要底层系统支持动态代码生成,对操作系统来说这意味着要支持动态分配带有“可写可执行”权限的内存页。当一个应用程序拥有请求分配可写可执行内存页的权限时,它会比较容易受到攻击从而允许任意代码动态生成并执行,这样就让恶意代码更容易有机可乘。

Apple 出于安全上的考虑,禁止了第三方 APP 使用 JSC 时开启 JIT,这些特点在 React Native 的 JS Runtime 页面[4]也有过相关的解释。不过在实际应用中,不做重 CPU 的运算只当胶水语言使用,JSC 还是绰绰有余了。

上面的讨论都是针对 iOS 系统的,在 Android 系统上,JSC 的表现就不尽人意了。

JSC 并没有对 Android 机型做很好的适配,虽然可以开启 JIT,但是性能表现并不好,这也是 Facebook 决心制作 Hermes 的一个原因,具体的性能对比分析可见本文的 Hermes 小节。

最后再说说 JSC 的调试支持情况。如果是 iOS 平台,我们可以直接用 Safari 的 debbuger 功能调试,如果是 Android 平台,目前我还没有找到一个很好的真机调试方法。

综合来看,JavaScriptCore 在 iOS 平台上有非常明显的主场优势,各个指标都是很优秀的,但在 Android 上因为缺乏优化,表现并不是很好。

2.V8

mobile_JSVM_V8

V8,我想我不用过多解释了,JavaScript 能有如今的地位,V8 功不可没。性能没得说,开启 JIT 后就是业内最强(不止是 JS),有很多介绍 V8 的文章,我这里就不多描述了,我们这里说说 V8 在移动端的表现。

同样作为 Google 家的产品,每一台 Android 手机上都安装了基于 Chromium 的 WebView,V8 也一并捆绑了。但是 V8 和 Chromium 捆绑的太紧密了,不像 iOS 上的 JavaScriptCore 封装为系统库可以被所有 App 调用。这就导致你想在 Android 上用 V8 还得自己封装,社区比较出名的项目是 J2V8[5],提供了 V8 的 Java bindings 案例。

V8 性能没得说,Android 上可以开启 JIT,但这些优势都是有代价的:开启 JIT 后内存占用高,并且 V8 的包体积也不小(大概 7 MB 左右),如果作为只是画 UI 的 Hybrid 系统,还是有些奢侈了。

我们再说说 V8 在 iOS 上的集成。

V8 在 2019 年推出了 JIT-less V8[6],也就是关闭 JIT 只使用 Ignition interpreter 解释执行 JS 文件,那么我们在 iOS 上集成 V8 就成了可能,因为 Apple 还是支持接入只有解释器功能的虚拟机引擎的。但是个人认为关闭了 JIT 的 V8 接入 iOS 价值不大,因为只开启解释器的话,这时候的 V8 和 JSC 的性能其实是差不多的,引入反而会增加一定的体积开销。

V8 还有一个有意思的特性很少人提及,那就是——堆快照(Heap snapshots),这个是 V8 在 2015[7] 年就支持的功能,但是社区里很少有人讨论它。

堆快照是什么原理呢?一般来说 JSVM 启动后,第一步往往是解析 JS 文件,这个还是比较耗时的,V8 支持预先生成 Heap snapshots,然后直接加载到堆内存中,快速的获得 JS 的初始化上下文。跨平台框架 NativeScript[8] 就利用了这样的技术,可以让 JS 的加载速度提升 3 倍,技术细节可以看他们的博文[9]。

V8_heap_snapshots

V8 真机调试也需要引入第三方库,Android 端社区上有人对 J2V8 做了 Chrome 调试协议的扩展,即 J2V8-Debugger[10] 项目,iOS 我没有找到相关的项目,可能需要自己实现一套扩展。

综合来看 V8 的确是 JSVM 中的性能王者,Android 端使用时可以完全发挥它的威力,但是 iOS 平台因为主场劣势,并不是很推荐。

3.Hermes

mobile_JSVM_hermes

Hermes 是 FaceBook 2019 年中旬开源的一款 JS 引擎,从 release[11] 记录可以看出,这个是专为 React Native 打造的 JS 引擎,可以说从设计之初就是为 Hybrid UI 系统打造。

Hermes 一开始推出就是要替代原来 RN Android 端的 JS 引擎,即 JavaScriptCore(因为 JSC 在 Android 端表现太拉垮了)。我们可以理一下时间线,FaceBook 自从 2019-07-12 宣布 Hermes 开源[12]后,jsc-android[13] 的维护信息就永远的停在了 2019-06-25[14],这个信号暗示得非常的明显:JavaScriptCore Android 我们不再维护啦,大家都去用我们做的 Hermes 啊。

最近 Hermes 已经计划伴随 React Native 0.64 版本登录 iOS 平台了,但是 RN 版本更新 blog 还没有出,大家可以看看我之前对 Apple 开发者协议的解读:Apple Agreement 3.3.2 规范解读,在这里我就不多说了。

Hermes 的特点主要是两个,一个是不支持 JIT,一个是支持直接生成/加载字节码,我们在下面分开讲一下。

Hermes 不支持 JIT 的主要原因有两个:加入 JIT 后,JS 引擎启动的预热时间会变长,一定程度上会加长首屏 TTI[15](页面首次加载可交互时间),现在的前端页面都讲究一个秒开,TTI 还是个挺重要的测量指标。另一个问题上 JIT 会增加包体积和内存占用,Chrome 内存占用高 V8 还是要承担一定责任的。

因为不支持 JIT,Hermes 在一些 CPU 密集计算的领域就不占优势了,所以在 Hybrid 系统里,最优的解决方案就是充分发挥 JavaScript 胶水语言的作用,CPU 密集的计算(例如矩阵变换,参数加密等)放在 Native 里做,算好了再传递给 JS 表现在 UI 上,这样可以兼顾性能和开发效率。

Hermes 最引人瞩目的就是支持生成字节码了,我在之前的博文《