当前位置: 首页 > 科技观察

哪个是最好的移动JS引擎?美国硅谷找...

时间:2023-03-15 23:04:37 科技观察

本文转载自微信公众号“卤蛋实验室”,作者halocarbon。转载本文请联系卤蛋实验室公众号。在一般的移动开发场景下,每次应用的更新都是通过应用市场版本通过Native语言开发和分发来实现的。但市场瞬息万变,Nativelanguage在开发效率上存在一定的短板,从应用版本更新到应用市场审核发布再到用户下载更新总会存在一定的时间滞后,导致无法开发新的功能及时覆盖所有用户。为了解决这个问题,开发者一般会在项目中引入脚本语言来加快APP的开发进程。在移动端广泛使用的脚本语言是Lua和JavaScript。前者在游戏领域使用较多,后者在应用领域使用较多。本文主要是想探讨移动端双端(iOS&Android)的JavaScript引擎选择。由于个人水平有限,文章中总会有疏漏和不足之处。请给我你的建议。JS引擎选择要点JavaScript作为世界上最流行的脚本语言,其引擎实现非常多:Apple使用的JavaScriptCore,性能最强大的V8,还有最近很火的QuickJS……如何选择这些JS引擎中最合适的那个?个人认为有几个方面的考虑:性能:没什么好说的,越快越好。Size:JS引擎会增加包的大小。良好的JavaScript语法支持:支持的新语法越多越好调试方便:是否直接支持调试?还是需要自己编译实现调试工具链?应用市场平台规范:主要是iOS平台,该平台禁止带有JIT功能的应用集成虚拟机比较麻烦的是,以上几点并不是相互独立的:比如启用JIT的V8引擎的性能是肯定的最好的,但它的引擎很大,内存使用率也很高;QuickJS在国际上非常有统治力,因为没有JIT的加持,与有JIT的引擎相比平均性能差距5-10倍。接下来综合刚才的几点,选择JavaScriptCore、V8、Hermes和QuickJS这四个JSVM,说说它们的优点和特点,再说说它们的不足。JS引擎功能比较1.JavaScriptCoremobile_JSVM_JSCJavaScriptCore是WebKit默认的内嵌JS引擎。维基百科上没有独立条目。仅在WebKit入口[1]的三级目录中引入。个人感觉还是有点离谱,毕竟也是老牌的JS引擎。由于WebKit是苹果最先开源的,所以在苹果自家的Safari浏览器和WebView上都使用了WebKit引擎,尤其是在iOS系统上。由于Apple的限制,所有的网页都只能加载WebKit,所以WebKit已经达到了这样的地步。事实上,作为WebKit模块一部分的JSC,继政策的春风。垄断归垄断,其实JSC的表现还是可以的。很多人不知道JSC的JIT功能其实早于V8。十几年前是最好的JS引擎,后来被V8赶上了。JSC有一个主要优势。iOS7之后,JSC作为系统级Framework向开发者开放。也就是说,如果你的APP使用了JSC,只需要在项目中引入,包大小为0开销!在今天讨论的JS引擎中,JSC是最好的。JSC虽然开启JIT的性能非常好,但仅限于苹果女王的Safari浏览器和WKWebView。只有这两个地方默认开启了JIT功能。如果直接在项目中引入JSC,则关闭JIT功能。你为什么这么做?RednaxelaFX大佬[2]给出了非常专业的解释[3]:JIT编译需要底层系统支持动态代码生成。对于操作系统,这意味着支持动态分配“可写和执行”内存页面。当应用程序具有请求分配可写和可执行内存页的权限时,它更容易允许动态生成和执行任意代码,这使得恶意代码更容易利用它。出于安全原因,Apple禁止第三方应用程序在使用JSC时启用JIT。这些功能也已在ReactNative的JSRuntime页面[4]中进行了解释。但是在实际应用中,JSC如果只是作为一种胶水语言,不需要繁重的CPU运算,就已经绰绰有余了。以上讨论都是针对iOS系统的。在Android系统上,JSC的表现并不尽如人意。JSC不能很好地适应Android模型。虽然可以开启JIT,但是性能不好。这也是Facebook决心做爱马仕的原因之一。具体的性能对比分析可以参见本文爱马仕部分。最后说一下JSC的调试支持。如果是iOS平台,我们可以直接使用Safari的调试器功能进行调试。如果是Android平台,我还没有找到很好的真机调试方法。综合来看,JavaScriptCore在iOS平台上的主场优势非常明显,各项指标都非常出色,但在Android上由于缺乏优化,表现不是很好。2、V8mobile_JSVM_V8V8,我觉得不用解释太多,JavaScript能有今天的地位,V8功不可没。性能没什么好说的。启用JIT后,是业界最强的(不仅仅是JS)。介绍V8的文章很多,这里就不赘述了。再说说V8在移动端的表现。同样作为谷歌产品,基于Chromium的WebView安装在每部Android手机上,V8也被捆绑。但V8和Chromium捆绑得太紧了,不像iOS上的JavaScriptCore,它被打包成一个系统库,所有应用程序都可以调用。这就导致了如果你想在Android上使用V8,你必须自己打包。社区中一个著名的项目是J2V8[5],它提供了Java绑定V8的案例。V8的性能没什么好说的。Android可以开启JIT,但这些优势是有代价的:开启JIT后内存占用高,V8的包体积也不小(7MB左右)。如果是只绘制UI的Hybrid系统还是有点奢侈。下面说一下V8在iOS上的集成。V8在2019年推出了JIT-lessV8[6],也就是关闭了JIT,只使用Ignition解释器来解释执行JS文件,那么我们在iOS上集成V8是有可能的,因为苹果还是支持访问onlyinterpreter虚拟机引擎的功能。不过我个人认为关闭JIT的V8接入iOS价值不大,因为如果只开启解释器,此时V8和JSC的性能其实差不多,引入会增加一定的体积开销量。V8还有一个很少被提及的有趣特性,那就是——堆快照(Heapsnapshots),这是V8在2015年支持的特性[7],但社区中很少有人讨论它。堆快照的原理是什么?一般来说,JSVM启动后,第一步往往是解析JS文件,比较耗时。V8支持预先生成Heap快照,然后直接加载到堆内存中,快速获取JS文件。初始化上下文。跨平台框架NativeScript[8]就使用了这样的技术,可以将JS的加载速度提高3倍。有关技术细节,请参阅他们的博客文章[9]。v8_heap_snapshotsV8真机调试也需要引入第三方库。Android社区有人扩展了J2V8的Chrome调试协议,即J2V8-Debugger[10]项目。iOS的相关项目我还没找到,可能需要自己实现一套扩展。.综合来看,V8确实是JSVM中的性能之王。在Android端使用可以充分发挥威力,但iOS平台由于居家劣势,不推荐使用。3.Hermesmobile_JSVM_hermesHermes是FaceBook在2019年年中开源的一个JS引擎。从release[11]记录可以看出这是一个专门为ReactNative打造的JS引擎。可以说从设计之初就是为HybridUI系统打造的。最初推出Hermes是为了替代RN的Android端原有的JS引擎,即JavaScriptCore(因为JSC在Android端的性能实在是太烂了)。我们可以看看时间线。自FaceBook于2019-07-12[12]宣布Hermes开源后,jsc-android[13]的维护信息于2019-06-25[14]永久停止。这个信号暗示非常明显:JavaScriptCoreAndroid不再由我们维护,大家应该使用我们的Hermes。近日,Hermes计划登陆iOS平台,ReactNative0.64版本,但RN版本更新博客暂未发布。大家可以看我之前对苹果开发者协议的解读:AppleAgreement3.3.2SpecificationInterpretation,这里我就不多说了。Hermes主要有两个特点,一个是不支持JIT,另一个是支持直接生成/加载字节码,下面我们分别讲。Hermes不支持JIT的原因主要有两个:加入JIT后,JS引擎启动的预热时间会变长,一定程度上会拉长首屏的TTI[15](交互时间firstpageload),而现在的前端页面都是关注一秒,TTI还是一个很重要的衡量指标。另一个问题是JIT会增加包的大小和内存使用量。对于Chrome的高内存占用,V8还是要承担一些责任的。由于不支持JIT,Hermes在一些CPU密集型计算领域并不具备优势,所以在Hybrid系统中,最优方案是充分发挥JavaScript胶水语言的作用,CPU密集型计算(如矩阵变换、参数加密等)在Native中完成,计算后传给JS显示在UI上,可以兼顾性能和开发效率。Hermes最引人注目的是它支持字节码生成。正如我在之前的博文中提到的,在Hermes加入AO??T之后,Babel、Minify、Parse、Compile的过程都是在开发者的电脑上完成的。直接发送字节码就可以让Hermes跑起来了。下面直接用demo来演示一下。Hermes先写一个test.js文件,里面随便写什么;然后编译Hermes的源码,编译过程可以直接按照文档[16]进行,这里略过。首先,Hermes支持直接解释运行JS代码,也就是正常的JS加载编译运行过程。在hermestest.js中,我们可以添加-emit-binary参数来试试生成字节码的功能:hermes-emit-binary-outtest.hbctest.js然后生成一个test.hbc字节码文件:hermes_bytecode最后,我们可以让Hermes直接加载并运行test.hbc文件:hermestest.hbc客观评价Hermes的字节码。首先,省去了JS引擎中解析编译的过程,JS代码的加载速度会大大加快,体现在UI上的TTI时间上。明显缩短;另一个优势Hermes字节码在设计时考虑了移动端的性能限制,支持增量加载而不是全量加载,对内存有限的低端安卓机更友好;但是字节码的大小会比原来的JS文件大,但是考虑到Hermes引擎本身的大小并不大,综合考虑这些体积增量还是可以接受的。关于Hermes的详细性能测试,网上有两篇文章写的不错:一篇是ReactNativeMemoryprofiling:JSCvsV8vsHermes[17]。可见爱马仕在安卓设备上的表现还是很不错的。而且JSC的性能很破:JSCvsV8vsHermes另一篇是携程的文章:携程对RN新一代JS引擎Hermes的研究,可以看出Hermes综合得分最高(JSC还是一样):JSVM_CPU_Performance说说性能吧Hermes的JS语法支持。Hermes主要支持ES6语法。Proxy刚开源的时候是不支持的,但是v0.7.0[18]已经支持了。他们的团队也更周到,不支持eval()和其他设计糟糕的API。我个人同意这种设计权衡。最后说一下Hermes的调试功能。目前Hermes已经支持Chrome的调试协议。我们可以直接使用Chrome的调试工具直接调试Hermes引擎。具体操作可以参考文档:使用GoogleChrome的DevTools在Hermes上调试JS[19]。移动端HybridUISystem构建的JS引擎。如果你想自己搭建一个Hybrid系统,Hermes是一个非常好的选择。4、在QuickJSmobile_JSVM_quickjs正式介绍QuickJS之前,先说说它的作者:FabriceBellard。软件界一直有这样的说法,一个高级程序员比20个平庸的程序员创造更多的价值,但是FabriceBellard不是高级程序员,他是天才。在我看来,他的创造力可以超过20个高级程序员,我们可以沿着时间轴[20]梳理一下他创造了什么:1997年,最快的计算圆周率算法发布,是Bailey-Borwein的变种-Plouffe公式,前者的时间复杂度为O(n^3),他优化为O(n^2),计算速度提升了43%。这是他在数学方面的成就。2000年FFmpeg发布,这是他在音视频领域的突破。2000年、2001年、2018年一举成名,3年3次获得国际混淆C代码大赛冠军。2002年,发布了TinyGL,这是他在图形领域取得的成就。2005年,发布QEMU,这是他在虚拟化领域的成果。2011年2019年,他使用JavaScript写了一个PC虚拟机Jslinux,一个运行在浏览器上的Linux操作系统。2019年,他发布了支持ES2020规范的JS虚拟机QuickJS。一个数量级之后,羡慕、嫉妒等情绪就会变成崇拜,贝拉德就是这样的人。收拾好心情,我们来看看QuickJS项目。QuickJS继承了FabriceBellard作品的一贯特点——小而强大。QuickJS很小,只有几个C文件,没有乱七八糟的第三方依赖。但是它的功能非常齐全,JS语法支持高达ES2020[21],Test262[22]测试表明QuickJS比V8有更高的语法支持。test262QuickJS性能如何?QuickJS官方有一个benchmark测试[23],综合比较多个JS引擎对同一个测试用例的跑分。下面是测试结果:JSVM_Benchmark结合上表和个人的一些测试,我们很容易得出一些结论:V8启用JIT的综合得分几乎是QuickJS的35倍,但是在同样的轻量级JS引擎中,QuickJS的表现还是很亮眼的。在内存占用方面,QuickJS远低于V8。毕竟JIT是个吃内存大户,而且QuickJS的设计对嵌入式系统非常友好(BellardAchievementTrophy🏆+1)QuickJS和Hermes这两个引擎的跑分相差无几。我私底下做过一些性能测试,两款引擎的性能也非常相似。由于QuickJS的设计,我对他和Lua的性能对比并不好奇。Lua是一门非常小巧玲珑的语言,在游戏领域和C/C++开发领域一直被用作胶水语言。个人写了一些测试用例,发现QuickJS和Lua的执行效率差不多。然后在网上找了一篇博文LuavsQuickJS[24]。这位老哥也做了一些测试,得出的结论是两者的性能差不多。在某些情况下,Lua会比QuickJS更快。官方文档中提到,QuickJS支持生成字节码[25],可以省去编译解析JS文件的过程。一开始我以为QuickJS和Hermes一样,可以直接生成字节码,然后交给QuickJS解释执行。自己编译后发现QuickJS和Hermes的机制不太一样:qjsc生成字节码的-e和-c选项,先把js文件生成一个字节码,再组装一个.在c文件中是这样的:#includeconstuint32_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没有官方的调试器,这意味着调试器语句将被忽略。社区已经有人实现了基于VSCode的调试器来支持vscode-quickjs-debug[26],不过我会对QuickJS做一些定制,期待官方对某个调试器协议的支持。从集成的角度来看,社区中已经有iOS[27]和Android[28]的示例工程,可以作为参考,对接自己的工程。总的来说,QuickJS是一个很有潜力的JS引擎。在高度支持JS语法的前提下,还将性能和体积优化到了极致。无论是HybridUI架构还是移动端的游戏脚本系统都可以考虑接入。选型思路1.单引擎单引擎是指iOS和Android统一使用一个引擎。这样就可以抹平JS层的差异,同样的JS代码不容易在iOS上跑起来。在Android上运行很好。只是一个奇怪的BUG出错了。结合市面上的跨端方案,大概有以下三种方案:统一使用JSC:这是ReactNative0.60之前的方案。统一使用Hermes:这是ReactNative0.64之后的设计方案。QuickJS的统一使用:QuickJS体积小,可以用来做一个非常轻量级的Hybrid系统。从上面可以看出,V8并没有被统一采用。这就是我之前所说的。V8在iOS平台上没有主场优势。关闭JIT后,性能和JSC差不多,包体积也会变大。好买卖。2.dual-engine双引擎也很好理解,就是iOS和Android各自使用,优点是可以发挥各自的主场优势,缺点是双端运行的结果可能是由于平台不一致导致的不一致,目前的解决方案有几种:JSCforiOS,V8forAndroid:Weex,NativeScript都是这样,可以在包大小和性能上有更好的平衡JSCforiOS,HermesforAndroid:ReactNatvie目前iOS的解决方案是JSC,Android的QuickJS:滴滴的跨终端框架hummer[29]就是这样的设计。从选型上看,iOS选择了JSC,Android有自己的选择,但是充分发挥了两个平台的特点:)3.Debugging不管是单引擎还是双引擎,综合业务开发经验也很重要。对于内置调试器功能的引擎来说,一切都好办,但是对于没有实现调试协议的引擎来说,缺少调试器还是会影响体验。但这并非不可能。一般来说,我们可以用曲线救国,类似于ReactNative中RemoteJSDebugging的思路:我们可以添加一个开关,将JS代码通过websocket发送给Chrome的WebWorker,然后使用Chrome的V8进行调试。.这样做的好处是可以调整一些业务bug,缺点是会引入另外一个JS引擎。如果出现一些引擎实现错误,将很难调试。好在这种情况是非常非常少见的,我们总不能因为噎着而放弃吃饭吧?小结本文从性能、体积、调试方便性等角度分析了JavaScriptCore、V8、Hermes和QuickJS这四种JS引擎,并分别分析了它们的不足和弱点。如果你对移动端JS引擎的选择感到困惑,我想从这篇文章入手,还是可以启发很多人的。希望我的这篇文章能对大家有所帮助。参考链接跨端框架的核心技术是什么?如何隐藏您的热更新包文件?深入理解JSCore[30]QuickJS引擎一年经验[31]