
高级前端进阶前言在一般的移动端开发场景中,每次更新应用功能都是通过Native语言开发并通过应用市场版本分发来实现的。但是市场瞬息万变,Native语言在开发效率上存在一定不足,并且从APP版本更新到......
高级前端进阶
前言在一般的移动端开发场景中,每次更新应用功能都是通过Native语言开发并通过应用市场版本分发来实现的。
但是市场瞬息万变,Native语言在开发效率上存在一定不足,并且从APP版本更新到应用市场审核发布再到用户下载更新,总会存在一定的时间差,这样就导致新的功能无法及时覆盖全量用户。
为了解决这个问题,开发者们一般会在项目里引入一门脚本语言,提速APP的研发流程。
在移动端应用比较广泛的脚本语言有Lua和JavaScript,前者在游戏领域用的比较多,后者在应用领域用的比较多。本篇文章主要是想探讨一下移动双端(iOSAndroid)的JavaScript引擎选型。由于个人水平有限,文章总会有遗漏和不足的地方,还请各位大佬多多指教。
JS引擎选型要点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.JavaScriptCoreJavaScriptCore是WebKit默认的内嵌JS引擎,wikipedia上都没有独立的词条,只在WebKit词条的三级目录里介绍了一下,个人感觉还是有些不像话,毕竟也是老牌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大佬给出过非常专业的解释:
Apple出于安全上的考虑,禁止了第三方APP使用JSC时开启JIT,这些特点在ReactNative的JSRuntime页面也有过相关的解释。不过在实际应用中,不做重CPU的运算只当胶水语言使用,JSC还是绰绰有余了。
上面的讨论都是针对iOS系统的,在Android系统上,JSC的表现就不尽人意了。
JSC并没有对Android机型做很好的适配,虽然可以开启JIT,但是性能表现并不好,这也是Facebook决心制作Hermes的一个原因,具体的性能对比分析可见本文的Hermes小节。
最后再说说JSC的调试支持情况。如果是iOS平台,我们可以直接用Safari的debbuger功能调试,如果是Android平台,目前我还没有找到一个很好的真机调试方法。
综合来看,JavaScriptCore在iOS平台上有非常明显的主场优势,各个指标都是很优秀的,但在Android上因为缺乏优化,表现并不是很好。
2.V8V8,我想我不用过多解释了,JavaScript能有如今的地位,V8功不可没。性能没得说,开启JIT后就是业内最强(不止是JS),有很多介绍V8的文章,我这里就不多描述了,我们这里说说V8在移动端的表现。
同样作为Google家的产品,每一台Android手机上都安装了基于Chromium的WebView,V8也一并捆绑了。但是V8和Chromium捆绑的太紧密了,不像iOS上的JavaScriptCore封装为系统库可以被所有App调用。这就导致你想在Android上用V8还得自己封装,社区比较出名的项目是J2V8,提供了V8的Javabindings案例。
V8性能没得说,Android上可以开启JIT,但这些优势都是有代价的:开启JIT后内存占用高,并且V8的包体积也不小(大概7MB左右),如果作为只是画UI的Hybrid系统,还是有些奢侈了。
我们再说说V8在iOS上的集成。
V8在2019年推出了JIT-lessV8,也就是关闭JIT只使用Ignitioninterpreter解释执行JS文件,那么我们在iOS上集成V8就成了可能,因为Apple还是支持接入只有解释器功能的虚拟机引擎的。但是个人认为关闭了JIT的V8接入iOS价值不大,因为只开启解释器的话,这时候的V8和JSC的性能其实是差不多的,引入反而会增加一定的体积开销。
V8还有一个有意思的特性很少人提及,那就是——堆快照(Heapsnapshots),这个是V8在2015年就支持的功能,但是社区里很少有人讨论它。
堆快照是什么原理呢?一般来说JSVM启动后,第一步往往是解析JS文件,这个还是比较耗时的,V8支持预先生成Heapsnapshots,然后直接加载到堆内存中,快速的获得JS的初始化上下文。跨平台框架NativeScript就利用了这样的技术,可以让JS的加载速度提升3倍,技术细节可以看他们的博文。
V8真机调试也需要引入第三方库,Android端社区上有人对J2V8做了Chrome调试协议的扩展,即J2V8-Debugger项目,iOS我没有找到相关的项目,可能需要自己实现一套扩展。
综合来看V8的确是JSVM中的性能王者,Android端使用时可以完全发挥它的威力,但是iOS平台因为主场劣势,并不是很推荐。
3.HermesHermes是FaceBook2019年中旬开源的一款JS引擎,从release[11]记录可以看出,这个是专为ReactNative打造的JS引擎,可以说从设计之初就是为HybridUI系统打造。
Hermes一开始推出就是要替代原来RNAndroid端的JS引擎,即JavaScriptCore(因为JSC在Android端表现太拉垮了)。我们可以理一下时间线,FaceBook自从2019-07-12宣布Hermes开源后,jsc-android的维护信息就永远的停在了2019-06-25,这个信号暗示得非常的明显:JavaScriptCoreAndroid我们不再维护啦,大家都去用我们做的Hermes啊。
最近Hermes已经计划伴随版本登录iOS平台了,但是RN版本更新blog还没有出,大家可以看看我之前对Apple开发者协议的解读:规范解读,在这里我就不多说了。
Hermes的特点主要是两个,一个是不支持JIT,一个是支持直接生成/加载字节码,我们在下面分开讲一下。
Hermes不支持JIT的主要原因有两个:加入JIT后,JS引擎启动的预热时间会变长,一定程度上会加长首屏TTI(页面首次加载可交互时间),现在的前端页面都讲究一个秒开,TTI还是个挺重要的测量指标。另一个问题上JIT会增加包体积和内存占用,Chrome内存占用高V8还是要承担一定责任的。
因为不支持JIT,Hermes在一些CPU密集计算的领域就不占优势了,所以在Hybrid系统里,最优的解决方案就是充分发挥JavaScript胶水语言的作用,CPU密集的计算(例如矩阵变换,参数加密等)放在Native里做,算好了再传递给JS表现在UI上,这样可以兼顾性能和开发效率。
Hermes最引人瞩目的就是支持生成字节码了,我在之前的博文《?跨端框架的核心技术到底是什么?》也提到过,Hermes加入AOT后,Babel、Minify、Parse和Compile这些流程全部都在开发者电脑上完成,直接下发字节码让Hermes运行就行,我们直接用个demo演示一下。
先写个的文件,里面随便写点啥都行;然后编译一下Hermes的源码,编译过程直接按文档来就行,我这里就略过了。
首先Hermes支持直接解释运行JS代码,就是正常的JS加载编译运行流程。
我们可以加入-emit-binary参数尝试一下生成Bytecode的功能:
然后就会生成一份字节码文件:
最后我们可以让Hermes直接加载运行文件:
客观评价一下Hermes的字节码,首先省去了在JS引擎里解析编译的流程,JS代码的加载速度将会大大加快,体现在UI上就是TTI时间会明显缩短;另一个优势Hermes的字节码在设计时就考虑了移动端的性能限制,支持增量加载而不是全量加载,对内存受限的中低端Android机更友好;不过字节码的体积会比原来的JS文件会大一些,但是考虑到Hermes引擎本身体积就不大,综合考虑下来这些体积增量还是可以接受的。
关于详细的Hermes性能测试情况,网上有两篇文章写的比较好:一篇是ReactNativeMemoryprofiling:JSCvsV8vsHermes,可以看到在Android设备上Hermes的表现还是很优异的,而JSC的表现非常拉垮:
另一篇是携程的文章:携程对RN新一代JS引擎Hermes的调研,可以看出Hermes综合成绩最高(JSC还是一样的拉垮):
说完性能我们再说说Hermes的JS语法支持情况。
Hermes主要支持的是ES6语法,刚开源时不支持Proxy,不过已经支持了。他们的团队也比较有想法,不支持witheval()等这种属于设计糟粕的API,这种设计的权衡我个人还是比较认同的。
最后我们谈谈Hermes的调试功能。
目前Hermes已经支持了Chrome的调试协议,我们可以直接用Chrome的debugging工具直接调试Hermes引擎,具体的操作可见文档:DebuggingJSonHermesusingGoogleChrome'sDevTools
综合来看,Hermes是一款专为移动端HybridUISystem打造的JS引擎,如果要自建一套Hybrid系统,Hermes是一个非常好的选择。
4.QuickJS正式介绍QuickJS前我们先说说它的作者:FabriceBellard。
软件界一直有个说法,一个高级程序员创造的价值可以超过20个平庸的程序员,但FabriceBellard不是高级程序员,他是天才,在我看来他的创造力可以超过20个高级程序员,我们可以顺着时间轴理一下他创造过些什么
1997年,发布了最快速的计算圆周率的算法,此算法是Bailey-Borwein-Plouffe公式的变体,前者的时间复杂度是O(n^3),他给优化成了O(n^2),使得计算速度提高了43%,这是他在数学上的成就
2000,2001,2018三年三度获得国际混淆C代码大赛
2002年,发布了TinyGL,这是他在图形学领域的成就
2005年,发布了QEMU,这是他在虚拟化领域的成就
2011年,他用JavaScript写了一个PC虚拟机Jslinux,一个跑在浏览器上的Linux操作系统
2019年,发布了QuickJS,一个支持ES2020规范的JS虚拟机
当人和人之间的差距差了几个数量级后,羡慕嫉妒之类的情绪就会转变为崇拜了,Bellard就是一个这样的人。
收复一下心情,我们来看一下QuickJS这个项目。QuickJS继承了FabriceBellard作品的一贯特色——小巧而又强大。
QuickJS体积非常小,只有几个C文件,没有乱七八糟的第三方依赖。但是他的功能又非常完善,JS语法支持到ES2020,Test262的测试显示,QuickJS的语法支持度比V8还要高。
那么QuickJS的性能如何呢?QuickJS官上有个基准测试,综合比较了多款JS引擎对同一测试用例的跑分情况。下面是测试结果:
JSVM_Benchmark
结合上面的表格和个人的一些测试,可以简单的得出一些结论:
开启JIT的V8综合评分差不多是QuickJS的35倍,但是在同等主打轻量的JS引擎中,QuickJS的性能还是很耀眼的
在内存占用上,QuickJS远低于V8,毕竟JIT是是吃内存的大户,而且QuickJS的设计对嵌入式系统很友好(Bellard成就奖杯?再+1)
QuickJS和Hermes的跑分情况是差不多的,我私下做了一些性能测试,这两个引擎的表现也很相近
因为QuickJS的设计,我不经好奇他和Lua的性能对比如何。
Lua是一门非常小巧精悍的语言,在游戏领域和C/C++开发中一直充当胶水语言的作用。
我个人写了一些测试用例,发现QuickJS和Lua的执行效率也是差不多的,后来在网上找到一篇博文LuavsQuickJS,这个老哥也做了一些测试,结论也是它俩的性能差不多,在部分场景Lua会比QuickJS快一些。
官方文档里有提到,QuickJS支持生成字节码,这样可以免去JS文件编译解析的过程。
我一开始以为QuickJS和Hermes一样,可以直接生成字节码,然后交给QuickJS解释执行。后来自己编译了一下才发现,QuickJS的作用机制和Hermes还不太一样:qjsc生成字节码的-e和-c选项,都是先把js文件生成一份字节码,然后拼到一个.c文件里,大概长下面的这个样子:
#includequickjs/_tqjsc_hello_size=87;//JS文件编译生成的字节码都在这个数组里constuint8_tqjsc_hello[87]={0x02,0x04,0x0e,0x63,0x6f,0x6e,0x73,0x6f,0x6c,0x65,0x06,0x6c,0x6f,0x67,0x16,0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x22,0x65,0x78,0x61,0x6d,0x70,0x6c,0x65,0x73,0x2f,0x68,0x65,0x6c,0x6c,0x6f,0x2e,0x6a,0x73,0x0e,0x00,0x06,0x00,0x9e,0x01,0x00,0x01,0x00,0x03,0x00,0x00,0x14,0x01,0xa0,0x01,0x00,0x00,0x00,0x39,0xf1,0x00,0x00,0x00,0x43,0xf2,0x00,0x00,0x00,0x04,0xf3,0x00,0x00,0x00,0x24,0x01,0x00,0xd1,0x28,0xe8,0x03,0x01,0x00,};intmain(intargc,char**argv){JSRuntime*rt;JSContext*ctx;rt=JS_NewRuntime();ctx=JS_NewContextRaw(rt);JS_AddIntrinsicBaseObjects(ctx);js_std_add_helpers(ctx,argc,argv);js_std_eval_binary(ctx,qjsc_hello,qjsc_hello_size,0);js_std_loop(ctx);JS_FreeContext(ctx);JS_FreeRuntime(rt);return0;}因为这是个.c文件,想跑起来还得再编译一次生成二进制文件。
从字节码这个设计点来看,QuickJS和Hermes的定位还是不太一样的。
虽然直接生成字节码可以大大减少JS文本文件的解析时间,但是QuickJS还是更偏嵌入式一些,生成的字节码放在一个C文件中,还需要进行编译才能运行;Hermes为ReactNative而生,生成的字节码一开始就考虑到分发功能(热更新就是一个应用场景),支持字节码的直接加载运行,不需要再编译一次。
上面主要还是对性能的考量,下面我们看看开发体验。
首先是QuickJS的调试功能支持。到目前为止(2021-02-22),QuickJS还没有官方的调试器,也就是说debugger语句会被忽略,社区有人实现了一套基于VSCode的调试器支持vscode-quickjs-debug,但是会对QuickJS做一些定制,个人还是蛮期待官方支持某个调试器协议的。
从集成的角度上看,社区上已经有了iOS和Android的示例项目,可以拿来用来参考接入到自己的工程中。
综合来看,QuickJS是一款潜力非常大的JS引擎,在JS语法高度支持的前提下,还把性能和体积都优化到了极致。在移动端的HybridUI架构和游戏脚本系统都可以考虑接入。
选型思路1.单引擎单引擎的意思就是iOS端和Android端统一采用一个引擎,这样做的话在JS层差异可以抹平,不容易出现同一份JS代码在iOS上运行是好的,Android上就出错的奇异BUG。结合市面上的跨端方案,大概有下面三种选型:
统一采用JSC:这个是之前的方案
统一使用Hermes:这个是之后的设计方案
统一采用QuickJS:QuickJS体积很小,可以用来制作非常轻量的Hybrid系统
上面看出没有统一采用V8,这个就是我前面说的,V8在iOS平台没有主场优势,关闭JIT后性能和JSC差不多,还会增大包体积,并不是很划算。
2.双引擎双引擎也很好理解,就是iOS端和Android端各用各的,优点是可以发挥各自的主场优势,缺点是可能会因为平台不一致导致双端运行结果不统一,现在的方案有这么几种:
iOS用JSC,Android用V8:Weex,NativeScript都是这样的,可以在包体积和性能上有较好的均衡
iOS用JSC,Android用Hermes:ReactNatvie现如今的方案
iOS用JSC,Android用QuickJS:滴滴的跨端框架hummer[29]就是这样的设计
从选型上看,iOS上都选择了JSC,Android各有各的选择,倒是充分发挥了两个平台的特色:)
3.调试无论是单引擎还是双引擎,集成后的业务开发体验也很重要。对于自带debugger功能的引擎来说一切都不在话下,但是对于没有实现调试协议的引擎来说,缺少debugger还是会影响体验的。
但不是也没有办法,一般来说我们可以曲线救国,类似于ReactNative的RemoteJSDebugging的思路:
我们可以加个开关,把JS代码通过websocket传送到Chrome的WebWorker,然后用Chrome的V8进行调试。这样做的优势是可以调整一些业务上的BUG,劣势就是又会引入一个JS引擎,万一遇到一些引擎实现的BUG,就很难debug了。不过好在这种情况非常非常少见,我们也不能因噎废食对吧。
总结本文从性能、体积、调试便捷性等功能点出发,分析了JavaScriptCore,V8,Hermes和QuickJS这4款JS引擎,分别分析了它们的缺点和弱点。如果大家有移动端JS引擎选型的困惑,我认为从本文出发,还是可以给不少人以灵感的,希望我的这篇文章能帮助到大家。
参考资料