公司动态 Company dynamics
开发动态 Exploitation dynamic
行业动态 Industry dynamics
浅谈JavaScriptCore
2018-08-28<h1 id="背景" style="text-align: left;"><a name="t0"></a>背景</h1><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">动态化作为移动客户端技术的一个重要分支,一直是业界积极探索的方向。目前业界流行的动态化方案,如Facebook的React Native,阿里巴巴的Weex都采用了前端系的DSL方案,而它们在iOS系统上能够顺利的运行,都离不开一个背后的功臣:JavaScriptCore(以下简称JSCore),它建立起了Objective-C(以下简称OC)和JavaScript(以下简称JS)两门语言之间沟通的桥梁。无论是这些流行的动态化方案,还是WebView Hybrid方案,亦或是之前广泛流行的JSPatch,JSCore都在其中发挥了举足轻重的作用。</p><p style="text-align: justify;"><br><span style="text-align: left;"></span></p><h1 id="从浏览器谈起" style="text-align: left;"><a name="t1"></a>从浏览器谈起</h1><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">在iOS7之后,JSCore作为一个系统级Framework被苹果提供给开发者。JSCore作为苹果的浏览器引擎WebKit中重要组成部分,这个JS引擎已经存在多年。如果想去追本溯源,探究JSCore的奥秘,那么就应该从JS这门语言的诞生,以及它最重要的宿主-Safari浏览器开始谈起。</p><p align="left"><span style="text-align: left;"> <br></span></p><h2 id="javascript历史简介" style="text-align: left;"><a name="t2"></a>JavaScript历史简介</h2><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">JavaScript诞生于1995年,它的设计者是Netscape的Brendan Eich,而此时的Netscape正是浏览器市场的霸主。</p><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">而二十多年前,当时人们在浏览网页的体验极差,那会儿的浏览器几乎只有页面的展示能力,没有和用户的交互逻辑处理能力。所以即使一个必填输入框传空,也需要经过服务端验证,等到返回结果之后才给出响应,再加上当时的网速很慢,可能半分钟过去了,返回的结果是告诉你某个必填字段未填。所以Brendan**花了十天写出了JavaScript**,由浏览器解释执行,从此之后浏览器也有了一些基本的交互处理能力,以及表单数据验证能力。</p><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">而Brendan可能没有想到,在二十多年后的今天。JS这门解释执行的动态脚本语言,不光成为前端届的“正统”,还入侵了后端开发领域,在编程语言排行榜上进入前三甲,仅次于Python和Java。而如何解释执行JS,则是各家引擎的核心技术。目前市面上比较常见的JS引擎有Google的V8(它被运用在Android操作系统以及Google的Chrome上),以及我们今天的主角–JSCore(它被运用在iOS操作系统以及Safari上)。</p><p align="left"><span style="text-align: left;"> <br></span></p><h2 id="webkit" style="text-align: left;"><a name="t3"></a>WebKit</h2><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">我们每天都会接触浏览器,使用浏览器进行工作、娱乐。让浏览器能够正常工作最核心的部分就是浏览器的内核,每个浏览器都有自己的内核,Safari的内核就是WebKit。WebKit诞生于1998年,并于2005年由Apple公司开源,Google的Blink也是在WebKit的分支上进行开发的。</p><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">WebKit由多个重要模块组成,通过下图我们可以对WebKit有个整体的了解: <br> <img title="" alt="图片1" src="https://user-gold-cdn.xitu.io/2018/8/24/16569b74d790cb3d?w=1186&h=912&f=jpeg&s=110226"></p><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">简单说,WebKit就是一个页面渲染以及逻辑处理引擎,前端工程师把HTML、JavaScript、CSS这“三驾马车”作为输入,经过WebKit的处理,就输出成了我们能看到以及操作的Web页面。从上图我们可以看出来,WebKit由图中框住的四个部分组成。而其中最主要的就是WebCore和JSCore(或者是其它JS引擎),这两部分我们会分成两个小章节详细讲述。除此之外,WebKit Embedding API是负责浏览器UI与WebKit进行交互的部分,而WebKit Ports则是让Webkit更加方便的移植到各个操作系统、平台上,提供的一些调用Native Library的接口,比如在渲染层面,在iOS系统中,Safari是交给CoreGraphics处理,而在Android系统中,Webkit则是交给Skia。</p><p align="left"><span style="text-align: left;"> <br></span></p><h3 id="webcore" style="text-align: left;"><a name="t4"></a>WebCore</h3><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">在上面的WebKit组成图中,我们可以发现只有WebCore是红色的。这是因为时至今日,WebKit已经有很多的分支以及各大厂家也进行了很多优化改造,唯独WebCore这个部分是所有WebKit共享的。WebCore是WebKit中代码最多的部分,也是整个WebKit中最核心的渲染引擎。那首先我们来看看整个WebKit的渲染流程:</p><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;"><img title="" alt="图片2" src="https://user-gold-cdn.xitu.io/2018/8/24/16569b7899215a42?w=1686&h=736&f=jpeg&s=87011"></p><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">首先浏览器通过URL定位到了一堆由HTML、CSS、JS组成的资源文件,通过加载器(这个加载器的实现也很复杂,在此不多赘述)把资源文件给WebCore。之后HTML Parser会把HTML解析成DOM树,CSS Parser会把CSS解析成CSSOM树。最后把这两棵树合并,生成最终需要的渲染树,再经过布局,与具体WebKit Ports的渲染接口,把渲染树渲染输出到屏幕上,成为了最终呈现在用户面前的Web页面。</p><p align="left"><span style="text-align: left;"> <br></span></p><h3 id="jscore" style="text-align: left;"><a name="t5"></a>JSCore</h3><p align="left"><span style="text-align: left;"> <b></b><b></b><br></span></p><p id="概述" style="text-align: left;">概述</p><p id="概述" style="text-align: left;"><span style="text-align: left;"><br></span></p><p style="text-align: justify;">终于讲到我们的主角 – JSCore。JSCore是WebKit默认内嵌的JS引擎,之所以说是默认内嵌,是因为很多基于WebKit分支开发的浏览器引擎都开发了自家的JS引擎,其中最出名的就是Chrome的V8。这些JS引擎的使命都相同,那就是解释执行JS脚本。而从上面的渲染流程图我们可以看到,JS和DOM树之间存在着互相关联,这是因为浏览器中的JS脚本最主要的功能就是操作DOM树,并与之交互。同样的,我们也通过一张图看下它的工作流程:</p><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;"><img title="" alt="图片3" src="https://user-gold-cdn.xitu.io/2018/8/24/16569b7c120f41a0?w=1088&h=1116&f=jpeg&s=126969"></p><p align="left"><span style="text-align: left;"> <br></span></p><p style="text-align: justify;">可以看到,相比静态编译语言生成语法树之后,还需要进行链接,装载生成可执行文件等操作,解释型语言在流程上要简化很多。这张流程图右边画框的部分就是JSCore的组成部分:Lexer、Parser、LLInt以及JIT的部分(之所以JIT的部分是用橙色标注,是因为并不是所有的JSCore中都有JIT部分)。整个工作流程主要分为以下三个部分:词法分析、语法分析以及解释执行。<span style="text-align: left;"></span></p><p style="text-align: justify;"><strong>PS:严格的讲,语言本身并不存在编译型或者是解释型,因为语言只是一些抽象的定义与约束,并不要求具体的实现,执行方式。这里讲JS是一门“解释型语言”只是JS一般是被JS引擎动态解释执行,而并不是语言本身的属性。</strong></p><p style="text-align: justify;"><b><br></b></p><h3 id="jscore值得注意的feature" style="text-align: left;"><a name="t6"></a>JSCore值得注意的Feature</h3><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">除了以上部分,JSCore还有几个值得注意的Feature。</p><p style="text-align: justify;"><br></p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><h4 id="基于寄存器的指令集结构" style="text-align: left;">基于寄存器的指令集结构</h4><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">JSCore采用的是基于寄存器的指令集结构,相比于基于栈的指令集结构(比如有些JVM的实现),因为不需要把操作结果频繁入栈出栈,所以这种架构的指令集执行效率更高。但是由于这样的架构也造成内存开销更大的问题,除此之外,还存在移植性弱的问题,因为虚拟机中的虚拟寄存器需要去匹配到真实机器中CPU的寄存器,可能会存在真实CPU寄存器不足的问题。</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">基于寄存器的指令集结构通常都是三地址或者二地址的指令集,例如:</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><pre class="prettyprint" style="text-align: left;" name="code"><code class="hljs oxygene has-numbering">i = a + b; <span class="hljs-comment">//转成三地址指令:</span> <span class="hljs-keyword">add</span> i,a,b; <span class="hljs-comment">//把a寄存器中的值和b寄存器中的值相加,存入i寄存器</span></code><ul class="pre-numbering" style="text-align: right;"><li>1</li><li>2</li><li>3</li></ul></pre><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">在三地址的指令集中的运算过程是把a和b分别mov到两个寄存器,然后把这两个寄存器的值求和之后,存入第三个寄存器。这就是三地址指令运算过程。</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">而基于栈的一般都是零地址指令集,因为它的运算不依托于具体的寄存器,而是使用对操作数栈和具体运算符来完成整个运算。</p><p style="text-align: justify;"><br></p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><h4 id="单线程机制" style="text-align: left;">单线程机制</h4><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">值得注意的是,整个JS代码是执行在一条线程里的,它并不像我们使用的OC、Java等语言,在自己的执行环境里就能申请多条线程去处理一些耗时任务来防止阻塞主线程。JS代码本身并不存在多线程处理任务的能力。但是为什么JS也存在多线程异步呢?强大的事件驱动机制,是让JS也可以进行多线程处理的关键。</p><p style="text-align: justify;"><br></p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><h4 id="事件驱动机制" style="text-align: left;">事件驱动机制</h4><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">之前讲到,JS的诞生就是为了让浏览器也拥有一些交互,逻辑处理能力。而JS与浏览器之间的交互是通过事件来实现的,比如浏览器检测到发生了用户点击,会传递一个点击事件通知JS线程去处理这个事件。 <br> 那通过这一特性,我们可以让JS也进行异步编程,简单来讲就是遇到耗时任务时,JS可以把这个任务丢给一个由JS宿主提供的工作线程(WebWorker)去处理。等工作线程处理完之后,会发送一个message让JS线程知道这个任务已经被执行完了,并在JS线程上去执行相应的事件处理程序。(但是需要注意,由于工作线程和JS线程并不在一个运行环境,所以它们并不共享一个作用域,故工作线程也不能操作window和DOM。)</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">JS线程和工作线程,以及浏览器事件之间的通信机制叫做事件循环(EventLoop),类似于iOS的runloop。它有两个概念,一个是Call Stack,一个是Task Queue。当工作线程完成异步任务之后,会把消息推到Task Queue,消息就是注册时的回调函数。当Call Stack为空的时候,主线程会从Task Queue里取一条消息放入Call Stack来执行,JS主线程会一直重复这个动作直到消息队列为空。</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;"><img title="" alt="图片5" src="https://user-gold-cdn.xitu.io/2018/8/24/16569b86cc6d4415?w=1340&h=884&f=jpeg&s=124789"></p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">以上这张图大概描述了JSCore的事件驱动机制,整个JS程序其实就是这样跑起来的。这个其实跟空闲状态下的iOS Runloop有点像,当基于Port的Source事件唤醒runloop之后,会去处理当前队列里的所有source事件。JS的事件驱动,跟消息队列其实是“异曲同工”。也正因为工作线程和事件驱动机制的存在,才让JS有了多线程异步能力。</p><p style="text-align: justify;"><br></p><h1 id="总结" style="text-align: left;"><a name="t15"></a>总结</h1><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">JSCore给iOS App提供了JS可以解释执行的运行环境与资源。对于我们实际开发而言,最主要的就是JSContext和JSValue这两个类。JSContext提供互相调用的接口,JSValue为这个互相调用提供数据类型的桥接转换。让JS可以执行Native方法,并让Native回调JS,反之亦然。</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;"><img title="" alt="图片11" src="https://user-gold-cdn.xitu.io/2018/8/24/16569b9eb9dc58bd?w=1230&h=422&f=png&s=41824"></p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">利用JSCore,我们可以做很多有想象空间的事。所有基于JSCore的Hybrid开发基本就是靠上图的原理来实现互相调用,区别只是具体的实现方式和用途不大相同。大道至简,只要正确理解这个基本流程,其它的所有方案不过是一些变通,都可以很快掌握。</p><p style="text-align: justify;"><br></p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><h1 id="一些引申阅读" style="text-align: left;"><a name="t16"></a>一些引申问题</h1><p style="text-align: justify;"><span style="text-align: left;"> </span></p><h2 id="jspatch的对象和方法没有实现jsexport协议js是如何调oc方法的" style="text-align: left;"><a name="t17"></a>JSPatch的对象和方法没有实现JSExport协议,JS是如何调OC方法的?</h2><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">JS调OC并不是通过JSExport。通过JSExport实现的方式有诸多问题,我们需要先写好Native的类,并实现JSExport协议,这个本身就不能满足“Patch”的需求。</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">所以JSPatch另辟蹊径,使用了OC的Runtime消息转发机制做这个事情,如下面这一个简单的JSPatch调用代码:</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p class="prettyprint" style="text-align: left;" name="code"><code class="hljs erlang has-numbering"><span class="hljs-function"><span class="hljs-title">require</span><span class="hljs-params">('<span class="hljs-variable">UIView</span>')</span> <span class="hljs-title">var</span> <span class="hljs-title">view</span> = UIV<span class="hljs-title">iew</span>.<span class="hljs-title">alloc</span><span class="hljs-params">()</span>.<span class="hljs-title">init</span><span class="hljs-params">()</span> </span></code></p><pre class="prettyprint" style="text-align: left;" name="code"><ul class="pre-numbering" style="text-align: right;"><li>1</li><li>2</li></ul></pre><p style="text-align: justify;"><span style="text-align: left;"> </span></p><ol style="text-align: left;"> <li><p style="text-align: justify;">require在全局作用域里生成UIView变量,来表示这个对象是一个OCClass。</p></li> <li><p style="text-align: justify;">通过正则把.alloc()改成._c(‘alloc’),来进行方法收口,最终会调用_methodFunc()把类名、对象、MethodName通过在Context早已定义好的Native方法,传给OC环境。</p></li> <li><p style="text-align: justify;">最终调用OC的CallSelector方法,底层通过从JS环境拿到的类名、方法名、对象之后,通过NSInvocation实现动态调用。</p></li> </ol><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">JSPatch的通信并没有通过JSExport协议,而是借助JSCore的Context与JSCore的类型转换和OC的消息转发机制来完成动态调用,实现思路真的很巧妙。</p><p style="text-align: justify;"><br></p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><h2 id="桥方法的实现是怎么通过jscore交互的" style="text-align: left;"><a name="t18"></a>桥方法的实现是怎么通过JSCore交互的?</h2><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">市面上常见的桥方法调用有两种:</p><p style="text-align: justify;"><span style="text-align: left;"> </span></p><ol style="text-align: left;"> <li><p style="text-align: justify;">1、通过UIWebView的delegate方法:shouldStartLoadWithRequest来处理桥接JS请求。JSRequest会带上methodName,通过WebViewBridge类调用该method。执行完之后,会使用WebView来执行JS的回调方法,当然实际上也是调用的WebView中的JSContext来执行JS,完成整个调用回调流程。</p></li> <li><p style="text-align: justify;">2、通过UIWebView的delegate方法:在webViewDidFinishLoadwebViewDidFinishLoad里通过KVC的方式获取UIWebView的JSContext,然后通过这个JSContext设置已经准备好的桥方法供JS环境调用。</p></li> </ol><p style="text-align: justify;"><span style="text-align: left;"> </span></p><h1 id="参考资料" style="text-align: left;"><a name="t19"></a>参考资料</h1><p style="text-align: justify;"><span style="text-align: left;"> </span></p><ol style="text-align: left;"> <li>《JavaScript高级程序设计》</li> <li><a href="http://webkithacks.github.io/webkit_presentations/architecture/#title-slide" target="_blank" rel="nofollow">Webkit Architecture</a></li> <li><a href="http://rednaxelafx.iteye.com/blog/492667" target="_blank" rel="nofollow">虚拟机随谈1:解释器…</a></li> <li><a href="http://www.starming.com/2017/10/11/deeply-analyse-webkit/" target="_blank" rel="nofollow">戴铭:深入剖析 WebKit</a><br></li><li><a href="https://trac.webkit.org/wiki/JavaScriptCore" target="_blank" rel="nofollow">JSCore-Wiki</a><br></li><li><a href="https://zhuanlan.zhihu.com/p/34646281" target="_blank" rel="nofollow">[知乎Tw93]iOS中的JSCore</a></li> </ol><p style="text-align: justify;"><span style="text-align: left;"> </span></p><h1 id="作者简介" style="text-align: left;"><a name="t20"></a>作者简介</h1><p style="text-align: justify;"><span style="text-align: left;"> </span></p><p style="text-align: justify;">唐笛,美团点评高级工程师。2017年加入原美团,目前作为外卖iOS团队主力开发,主要负责移动端基础设施建设,动态化等方向相关推进工作,致力于提升移动端研发效率与研发质量。</p><p style="text-align: justify;"><u></u><br></p><h1><font face="宋体">公司</font><font face="宋体">简介</font></h1><font face="宋体"><p align="justify" style="text-align: justify;"><span><font face="Helvetica"><b></b><b></b><br>比牛科技,一家优质的软件技术服务商。</font></span><span></span></p><p align="justify" style="text-align: justify;"><span><font face="Helvetica">擅长小程序开发、</font>Web应用开发,app开发......</span><span></span></p><p align="justify" style="text-align: justify;"><span><font face="Helvetica">为您提供以客户为中心,让客户满意的软件定制服务!</font></span><span></span></p><p><br></p></font><p style="text-align: justify;"><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike><b></b><u></u><u></u><br></p><p style="text-align: justify;"><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike><b></b><u></u><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike><br></p><p align="left"><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike><b></b><i></i><u></u><sub></sub><sup></sup><strike></strike><br></p>
栏目最新信息























