動態化技術如果不知道它,你就白學了!

關注公衆號:wingjay

第 17 篇

人和人的差距,是下班後的 4 小時拉開的。

 

 

 

讀者朋友,你好,我是 wingjay。

 

之前的文章《2019年移動開發,我的求變之路》裏,我提到動態化方向是 2019 年移動領域一個重要的方向,也是本公衆號着重關注的技術點之一。而目前來看,動態化領域的一大主流技術就是基於 JavaScript 與 Native 進行通信,藉助 Js 本身的動態性來實現業務邏輯與視圖的動態化。而這背後,離不開一個強大的後盾:JavaScriptCore。

 

熟悉 Java 或 Android 的讀者都知道,Java 代碼是運行在虛擬機(如JVM / Davilk)上的,而 JsCore 就相當於是 Js 的虛擬機。它正是這套動態化方案的核心所在。

 

WebView 大家都清楚,它內部包含兩大模塊:WebCore 和 JsCore,前者用來解析渲染 Html 和 CSS,後者則是用來解析執行 Js 代碼。而 RN 和 Weex 這類技術方案和 WebView 本質區別在於:前者拋棄了繁重的 WebCore 和 Html 語法,而是利用 React/Vue 自己實現了一套Native渲染方式。但兩者的共同點,就在於它們都依賴 JsCore 來執行 Js。

 

換句話說,沒有 JsCore,就不會誕生 RN、Weex 這類優秀的動態化技術。而今天,我們就順着動態化這條路線,一起來學習下 JsCore 的原理。今天的文章來自美團技術團隊:唐笛,文章很硬,大家可以收藏起來慢慢品味。

 

文章所有刪減以適合 Android 讀者閱讀,文末可查看原文。

正文

背景

動態化作爲移動客戶端技術的一個重要分支,一直是業界積極探索的方向。目前業界流行的動態化方案,如Facebook的React Native,阿里巴巴的Weex都採用了前端系的DSL方案,而它們在移動端系統上能夠順利的運行,都離不開一個背後的功臣:JavaScriptCore(以下簡稱JSCore),它建立起了 Java 和JavaScript(以下簡稱JS)兩門語言之間溝通的橋樑。無論是這些流行的動態化方案,還是WebView Hybrid方案,JSCore都在其中發揮了舉足輕重的作用。作爲一名移動端開發工程師,瞭解JSCore已經逐漸成爲了必備技能之一。

從瀏覽器談起

JSCore作爲瀏覽器引擎WebKit中重要組成部分,這個JS引擎已經存在多年。如果想去追本溯源,探究JSCore的奧祕,那麼就應該從JS這門語言的誕生,以及它最重要的宿主-Safari瀏覽器開始談起。

JavaScript歷史簡介

JavaScript誕生於1995年,它的設計者是Netscape的Brendan Eich,而此時的Netscape正是瀏覽器市場的霸主。

而二十多年前,當時人們在瀏覽網頁的體驗極差,因爲那會兒的瀏覽器幾乎只有頁面的展示能力,沒有和用戶的交互邏輯處理能力。所以即使一個必填輸入框傳空,也需要經過服務端驗證,等到返回結果之後纔給出響應,再加上當時的網速很慢,可能半分鐘過去了,返回的結果是告訴你某個必填字段未填。所以Brendan花了十天寫出了JavaScript,由瀏覽器解釋執行,從此之後瀏覽器也有了一些基本的交互處理能力,以及表單數據驗證能力。

而Brendan可能沒有想到,在二十多年後的今天。JS這門解釋執行的動態腳本語言,不光成爲前端屆的“正統”,還入侵了後端開發領域,在編程語言排行榜上進入前三甲,僅次於Python和Java。而如何解釋執行JS,則是各家引擎的核心技術。目前市面上比較常見的JS引擎有Google的V8(它被運用在Android操作系統以及Google的Chrome上),以及我們今天的主角JSCore。

WebKit

我們每天都會接觸瀏覽器,使用瀏覽器進行工作、娛樂。讓瀏覽器能夠正常工作最核心的部分就是瀏覽器的內核,每個瀏覽器都有自己的內核,Safari的內核就是WebKit。WebKit誕生於1998年,並於2005年由Apple公司開源,Google的Blink也是在WebKit的分支上進行開發的。

WebKit由多個重要模塊組成,通過下圖我們可以對WebKit有個整體的瞭解:

å¾ç1

簡單點講,WebKit就是一個頁面渲染以及邏輯處理引擎,前端工程師把HTML、JavaScript、CSS這“三駕馬車”作爲輸入,經過WebKit的處理,就輸出成了我們能看到以及操作的Web頁面。從上圖我們可以看出來,WebKit由圖中框住的四個部分組成。而其中最主要的就是WebCore和JSCore(或者是其它JS引擎),這兩部分我們會分成兩個小章節詳細講述。除此之外,WebKit Embedding API是負責瀏覽器UI與WebKit進行交互的部分,而WebKit Ports則是讓Webkit更加方便的移植到各個操作系統、平臺上,提供的一些調用Native Library的接口,比如在渲染層面,在Android系統中,Webkit則是交給Skia。

WebCore

在上面的WebKit組成圖中,我們可以發現只有WebCore是紅色的。這是因爲時至今日,WebKit已經有很多的分支以及各大廠家也進行了很多優化改造,唯獨WebCore這個部分是所有WebKit共享的。WebCore是WebKit中代碼最多的部分,也是整個WebKit中最核心的渲染引擎。那首先我們來看看整個WebKit的渲染流程:

å¾ç2

首先瀏覽器通過URL定位到了一堆由HTML、CSS、JS組成的資源文件,通過加載器(這個加載器的實現也很複雜,在此不多贅述)把資源文件給WebCore。之後HTML Parser會把HTML解析成DOM樹,CSS Parser會把CSS解析成CSSOM樹。最後把這兩棵樹合併,生成最終需要的渲染樹,再經過佈局,與具體WebKit Ports的渲染接口,把渲染樹渲染輸出到屏幕上,成爲了最終呈現在用戶面前的Web頁面。

JSCore

 

概述

終於講到我們這期的主角——JSCore。JSCore是WebKit默認內嵌的JS引擎,之所以說是默認內嵌,是因爲很多基於WebKit分支開發的瀏覽器引擎都開發了自家的JS引擎,其中最出名的就是Chrome的V8。這些JS引擎的使命都相同,那就是解釋執行JS腳本。而從上面的渲染流程圖我們可以看到,JS和DOM樹之間存在着互相關聯,這是因爲瀏覽器中的JS腳本最主要的功能就是操作DOM樹,並與之交互。同樣的,我們也通過一張圖看下它的工作流程:

å¾ç3

可以看到,相比靜態編譯語言生成語法樹之後,還需要進行鏈接,裝載生成可執行文件等操作,解釋型語言在流程上要簡化很多。這張流程圖右邊畫框的部分就是JSCore的組成部分:Lexer、Parser、LLInt以及JIT的部分(之所以JIT的部分是用橙色標註,是因爲並不是所有的JSCore中都有JIT部分)。接下來我們就搭配整個工作流程介紹每一部分,它主要分爲以下三個部分:詞法分析、語法分析以及解釋執行。

PS:嚴格的講,語言本身並不存在編譯型或者是解釋型,因爲語言只是一些抽象的定義與約束,並不要求具體的實現,執行方式。這裏講JS是一門“解釋型語言”只是JS一般是被JS引擎動態解釋執行,而並不是語言本身的屬性。

詞法分析:Lexer

詞法分析很好理解,就是把一段我們寫的源代碼分解成Token序列的過程,這一過程也叫分詞。在JSCore,詞法分析是由Lexer來完成(有的編譯器或者解釋器把分詞叫做Scanner)。

這是一句很簡單的C語言表達式:

sum = 3 + 2; 

將其標記化之後可以得到下表的內容:

元素 標記類型
sum 標識符
= 賦值操作符
3 數字
+ 加法操作符
2 數字
; 語句結束

這就是詞法分析之後的結果,但是詞法分析並不會關注每個Token之間的關係,是否匹配,僅僅是把它們區分開來,等待語法分析來把這些Token“串起來”。詞法分析函數一般是由語法分析器(Parser)來進行調用的。在JSCore中,詞法分析器Lexer的代碼主要集中在parser/Lexer.h、Lexer.cpp中。

語法分析:Parser

跟人類語言一樣,我們講話的時候其實是按照約定俗成,交流習慣按照一定的語法講出一個又一個詞語。那類比到計算機語言,計算機要理解一門計算機語言,也要理解一個語句的語法。例如以下一段JS語句:

var sum = 2 + 3;
var a = sum + 5;

Parser會把Lexer分析之後生成的token序列進行語法分析,並生成對應的一棵抽象語法樹(AST)。這個樹長什麼樣呢?在這裏推薦一個網站:esprima Parser,輸入JS語句可以立馬生成我們所需的AST。例如,以上語句就被生成這樣的一棵樹:

å¾ç4

之後,ByteCodeGenerator會根據AST來生成JSCore的字節碼,完成整個語法解析步驟。

解釋執行:LLInt和JIT

JS源代碼經過了詞法分析和語法分析這兩個步驟,轉成了字節碼,其實就是經過任何一門程序語言必經的步驟--編譯。但是不同於我們編譯運行OC代碼,JS編譯結束之後,並不會生成存放在內存或者硬盤之中的目標代碼或可執行文件。生成的指令字節碼,會被立即被JSCore這臺虛擬機進行逐行解釋執行。

運行指令字節碼(ByteCode)是JS引擎中很核心的部分,各家JS引擎的優化也主要集中於此。JSByteCode的解釋執行是一套很複雜的系統,特別是加入了OSR和多級JIT技術之後,整個解釋執行變的越來越高效,並且讓整個ByteCode的執行在低延時之間和高吞吐之間有個很好的平衡:由低延時的LLInt來解釋執行ByteCode,當遇到多次重複調用或者是遞歸,循環等條件會通過OSR切換成JIT進行解釋執行(根據具體觸發條件會進入不同的JIT進行動態解釋)來加快速度。由於這部分內容較爲複雜,而且不是本文重點,故只做簡單介紹,不做深入的討論。

JSCore值得注意的Feature

除了以上部分,JSCore還有幾個值得注意的Feature。

基於寄存器的指令集結構

JSCore採用的是基於寄存器的指令集結構,相比於基於棧的指令集結構(比如有些JVM的實現),因爲不需要把操作結果頻繁入棧出棧,所以這種架構的指令集執行效率更高。但是由於這樣的架構也造成內存開銷更大的問題,除此之外,還存在移植性弱的問題,因爲虛擬機中的虛擬寄存器需要去匹配到真實機器中CPU的寄存器,可能會存在真實CPU寄存器不足的問題。

基於寄存器的指令集結構通常都是三地址或者二地址的指令集,例如:

i = a + b;
//轉成三地址指令:
add i,a,b; //把a寄存器中的值和b寄存器中的值相加,存入i寄存器

在三地址的指令集中的運算過程是把a和b分別mov到兩個寄存器,然後把這兩個寄存器的值求和之後,存入第三個寄存器。這就是三地址指令運算過程。

而基於棧的一般都是零地址指令集,因爲它的運算不依託於具體的寄存器,而是使用對操作數棧和具體運算符來完成整個運算。

單線程機制

值得注意的是,整個JS代碼是執行在一條線程裏的,它並不像我們使用的OC、Java等語言,在自己的執行環境裏就能申請多條線程去處理一些耗時任務來防止阻塞主線程。JS代碼本身並不存在多線程處理任務的能力。但是爲什麼JS也存在多線程異步呢?強大的事件驅動機制,是讓JS也可以進行多線程處理的關鍵。

事件驅動機制

之前講到,JS的誕生就是爲了讓瀏覽器也擁有一些交互,邏輯處理能力。而JS與瀏覽器之間的交互是通過事件來實現的,比如瀏覽器檢測到發生了用戶點擊,會傳遞一個點擊事件通知JS線程去處理這個事件。

那通過這一特性,我們可以讓JS也進行異步編程,簡單來講就是遇到耗時任務時,JS可以把這個任務丟給一個由JS宿主提供的工作線程(WebWorker)去處理。等工作線程處理完之後,會發送一個message讓JS線程知道這個任務已經被執行完了,並在JS線程上去執行相應的事件處理程序。(但是需要注意,由於工作線程和JS線程並不在一個運行環境,所以它們並不共享一個作用域,故工作線程也不能操作window和DOM。)

JS線程和工作線程,以及瀏覽器事件之間的通信機制叫做事件循環(EventLoop),類似於Android的Looper。它有兩個概念,一個是Call Stack,一個是Task Queue。當工作線程完成異步任務之後,會把消息推到Task Queue,消息就是註冊時的回調函數。當Call Stack爲空的時候,主線程會從Task Queue裏取一條消息放入Call Stack來執行,JS主線程會一直重複這個動作直到消息隊列爲空。

å¾ç5

以上這張圖大概描述了JSCore的事件驅動機制,整個JS程序其實就是這樣跑起來的。這個其實跟空閒狀態下的Looper有點像,當基於Port的Source事件喚醒runloop之後,會去處理當前隊列裏的所有source事件。JS的事件驅動,跟消息隊列其實是“異曲同工”。也正因爲工作線程和事件驅動機制的存在,才讓JS有了多線程異步能力。

JSCore結構

實際上,即使同爲JSCore,它們之間也存在很多區別。因爲隨着JS這門語言的發展,JS的宿主越來越多,有各種各樣的瀏覽器,甚至是常見於服務端的Node.js(基於V8運行)。隨時使用場景的不同,以及WebKit團隊自身不停的優化,JSCore逐漸分化出不同的版本。除了老版本的JSCore,還有2008年宣佈的運行在Safari、WKWebView中的Nitro(SquirrelFish)等等。而在本文中,我們主要介紹移動端系統自帶的JSCore Framework。

筆者認爲很有必要了解的概念只有4個:JSVM、JSContext、JSValue、JSExport。鑑於講述這些概念的文章已經有很多,本文儘量從一些不同的角度(比如原理,延伸對比等)去解釋這些概念。

 

JSVirtualMachine

一個JSVirtualMachine(以下簡稱JSVM)實例代表了一個自包含的JS運行環境,或者是一系列JS運行所需的資源。該類有兩個主要的使用用途:一是支持併發的JS調用,二是管理JS和Native之間橋對象的內存。

JSVM是我們要學習的第一個概念。官方介紹JSVM爲JavaScript的執行提供底層資源,而從類名直譯過來,一個JSVM就代表一個JS虛擬機,我們在上面也提到了虛擬機的概念,那我們先討論一下什麼是虛擬機。首先我們可以看看(可能是)最出名的虛擬機——JVM(Java虛擬機),JVM主要做兩個事情:

  1. 首先它要做的是把JavaC編譯器生成的ByteCode(ByteCode其實就是JVM的虛擬機器指令)生成每臺機器所需要的機器指令,讓Java程序可執行(如下圖)。

  2. 第二步,JVM負責整個Java程序運行時所需要的內存空間管理、GC以及Java程序與Native(即C,C++)之間的接口等等。

å¾ç7

從功能上來看,一個高級語言虛擬機主要分爲兩部分,一個是解釋器部分,用來運行高級語言編譯生成的ByteCode,還有一部分則是Runtime運行時,用來負責運行時的內存空間開闢、管理等等。實際上,JSCore常常被認爲是一個JS語言的優化虛擬機,它做着JVM類似的事情,只是相比靜態編譯的Java,它還多承擔了把JS源代碼編譯成字節碼的工作。

既然JSCore被認爲是一個虛擬機,那JSVM又是什麼?實際上,JSVM就是一個抽象的JS虛擬機,讓開發者可以直接操作。在App中,我們可以運行多個JSVM來執行不同的任務。而且每一個JSContext(下節介紹)都從屬於一個JSVM。但是需要注意的是每個JSVM都有自己獨立的堆空間,GC也只能處理JSVM內部的對象(在下節會簡單講解JS的GC機制)。所以說,不同的JSVM之間是無法傳遞值的。

值得注意的還有,在上面的章節中,我們提到的JS單線程機制。這意味着,在一個JSVM中,只有一條線程可以跑JS代碼,所以我們無法使用JSVM進行多線程處理JS任務。如果我們需要多線程處理JS任務的場景,就需要同時生成多個JSVM,從而達到多線程處理的目的。

JS的GC機制

JS同樣也不需要我們去手動管理內存。JS的內存管理使用的是GC機制(Tracing Garbage Collection)。不同於OC的引用計數,Tracing Garbage Collection是由GCRoot(Context)開始維護的一條引用鏈,一旦引用鏈無法觸達某對象節點,這個對象就會被回收掉。如下圖所示:

å¾ç8

 

總結

JSCore給App提供了JS可以解釋執行的運行環境與資源。對於我們實際開發而言,最主要的就是JSContext和JSValue這兩個類。JSContext提供互相調用的接口,JSValue爲這個互相調用提供數據類型的橋接轉換。讓JS可以執行Native方法,並讓Native回調JS,反之亦然。

å¾ç11

利用JSCore,我們可以做很多有想象空間的事。所有基於JSCore的Hybrid開發基本就是靠上圖的原理來實現互相調用,區別只是具體的實現方式和用途不大相同。大道至簡,只要正確理解這個基本流程,其它的所有方案不過是一些變通,都可以很快掌握。

----

公衆號:wingjay

人和人的差距,是下班後的 4 小時拉開的。

長期輸出有價值 Android 技術內容,更重要的是,原創文章末尾會有隨機抽獎喲

----

 

在上一篇文章《說一件重要的事。》中可查看上一個“一文一題”。

 

更多文章:

2019年移動開發,我的求變之路

Flutter 跨平臺實踐及原理探祕

支付寶的 Hybrid 架構是如何建設起來的?

Android 架構之長連接技術

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章