您好,登錄后才能下訂單哦!
動態化作為移動客戶端技術的一個重要分支,一直是業界積極探索的方向。目前業界流行的動態化方案,如Facebook的React Native,阿里巴巴的Weex都采用了前端系的DSL方案,而它們在iOS系統上能夠順利的運行,都離不開一個背后的功臣:JavaScriptCore(以下簡稱JSCore),它建立起了Objective-C(以下簡稱OC)和JavaScript(以下簡稱JS)兩門語言之間溝通的橋梁。無論是這些流行的動態化方案,還是WebView Hybrid方案,亦或是之前廣泛流行的JSPatch,JSCore都在其中發揮了舉足輕重的作用。作為一名iOS開發工程師,了解JSCore已經逐漸成為了必備技能之一。
從瀏覽器談起
在iOS 7之后,JSCore作為一個系統級Framework被蘋果提供給開發者。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(它被運用在iOS操作系統以及Safari上)。
WebKit
我們每天都會接觸瀏覽器,使用瀏覽器進行工作、娛樂。讓瀏覽器能夠正常工作最核心的部分就是瀏覽器的內核,每個瀏覽器都有自己的內核,Safari的內核就是WebKit。WebKit誕生于1998年,并于2005年由Apple公司開源,Google的Blink也是在WebKit的分支上進行開發的。
WebKit由多個重要模塊組成,通過下圖我們可以對WebKit有個整體的了解:
簡單點講,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。
WebCore
在上面的WebKit組成圖中,我們可以發現只有WebCore是紅色的。這是因為時至今日,WebKit已經有很多的分支以及各大廠家也進行了很多優化改造,唯獨WebCore這個部分是所有WebKit共享的。WebCore是WebKit中代碼最多的部分,也是整個WebKit中最核心的渲染引擎。那首先我們來看看整個WebKit的渲染流程:
首先瀏覽器通過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樹,并與之交互。同樣的,我們也通過一張圖看下它的工作流程:
可以看到,相比靜態編譯語言生成語法樹之后,還需要進行鏈接,裝載生成可執行文件等操作,解釋型語言在流程上要簡化很多。這張流程圖右邊畫框的部分就是JSCore的組成部分:Lexer、Parser、LLInt以及JIT的部分(之所以JIT的部分是用橙色標注,是因為并不是所有的JSCore中都有JIT部分)。接下來我們就搭配整個工作流程介紹每一部分,它主要分為以下三個部分:詞法分析、語法分析以及解釋執行。
PS:嚴格的講,語言本身并不存在編譯型或者是解釋型,因為語言只是一些抽象的定義與約束,并不要求具體的實現,執行方式。這里講JS是一門“解釋型語言”只是JS一般是被JS引擎動態解釋執行,而并不是語言本身的屬性。
詞法分析:Lexer
詞法分析很好理解,就是把一段我們寫的源代碼分解成Token序列的過程,這一過程也叫分詞。在JSCore,詞法分析是由Lexer來完成(有的編譯器或者解釋器把分詞叫做Scanner)。
這是一句很簡單的C語言表達式:
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。例如,以上語句就被生成這樣的一棵樹:
之后,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),類似于iOS的runloop。它有兩個概念,一個是Call Stack,一個是Task Queue。當工作線程完成異步任務之后,會把消息推到Task Queue,消息就是注冊時的回調函數。當Call Stack為空的時候,主線程會從Task Queue里取一條消息放入Call Stack來執行,JS主線程會一直重復這個動作直到消息隊列為空。
以上這張圖大概描述了JSCore的事件驅動機制,整個JS程序其實就是這樣跑起來的。這個其實跟空閑狀態下的iOS Runloop有點像,當基于Port的Source事件喚醒runloop之后,會去處理當前隊列里的所有source事件。JS的事件驅動,跟消息隊列其實是“異曲同工”。也正因為工作線程和事件驅動機制的存在,才讓JS有了多線程異步能力。
iOS中的JSCore
iOS7之后,蘋果對WebKit中的JSCore進行了Objective-C的封裝,并提供給所有的iOS開發者。JSCore框架給Swift、OC以及C語言編寫的App提供了調用JS程序的能力。同時我們也可以使用JSCore往JS環境中去插入一些自定義對象。
iOS中可以使用JSCore的地方有多處,比如封裝在UIWebView中的JSCore,封裝在WKWebView中的JSCore,以及系統提供的JSCore。實際上,即使同為JSCore,它們之間也存在很多區別。因為隨著JS這門語言的發展,JS的宿主越來越多,有各種各樣的瀏覽器,甚至是常見于服務端的Node.js(基于V8運行)。隨時使用場景的不同,以及WebKit團隊自身不停的優化,JSCore逐漸分化出不同的版本。除了老版本的JSCore,還有2008年宣布的運行在Safari、WKWebView中的Nitro(SquirrelFish)等等。而在本文中,我們主要介紹iOS系統自帶的JSCore Framework。
iOS官方文檔 對JSCore的介紹很簡單,其實主要就是給App提供了調用JS腳本的能力。我們首先通過JSCore Framework的15個開放頭文件來“管中窺豹”,如下圖所示:
乍一看,概念很多。但是除去一些公共頭文件以及一些很細節的概念,其實真正常用的并不多,筆者認為很有必要了解的概念只有4個:JSVM、JSContext、JSValue、JSExport。鑒于講述這些概念的文章已經有很多,本文盡量從一些不同的角度(比如原理,延伸對比等)去解釋這些概念。
JSVirtualMachine
一個JSVirtualMachine(以下簡稱JSVM)實例代表了一個自包含的JS運行環境,或者是一系列JS運行所需的資源。該類有兩個主要的使用用途:一是支持并發的JS調用,二是管理JS和Native之間橋對象的內存。
JSVM是我們要學習的第一個概念。官方介紹JSVM為JavaScript的執行提供底層資源,而從類名直譯過來,一個JSVM就代表一個JS虛擬機,我們在上面也提到了虛擬機的概念,那我們先討論一下什么是虛擬機。首先我們可以看看(可能是)最出名的虛擬機——JVM(Java虛擬機),JVM主要做兩個事情:
首先它要做的是把JavaC編譯器生成的ByteCode(ByteCode其實就是JVM的虛擬機器指令)生成每臺機器所需要的機器指令,讓Java程序可執行(如下圖)。
第二步,JVM負責整個Java程序運行時所需要的內存空間管理、GC以及Java程序與Native(即C,C++)之間的接口等等。
從功能上來看,一個高級語言虛擬機主要分為兩部分,一個是解釋器部分,用來運行高級語言編譯生成的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)開始維護的一條引用鏈,一旦引用鏈無法觸達某對象節點,這個對象就會被回收掉。如下圖所示:
JSContext
一個JSContext表示了一次JS的執行環境。我們可以通過創建一個JSContext去調用JS腳本,訪問一些JS定義的值和函數,同時也提供了讓JS訪問Native對象,方法的接口。
JSContext是我們在實際使用JSCore時,經常用到的概念之一。"Context"這個概念我們都或多或少的在其它開發場景中見過,它最常被翻譯成“上下文”。那什么是上下文?比如在一篇文章中,我們看到一句話:“他飛快的跑了出去。”但是如果我們不看上下文的話,我們并不知道這句話究竟是什么意思:誰跑了出去?他是誰?他為什么要跑?
寫計算機理解的程序語言跟寫文章是相似的,我們運行任何一段語句都需要有這樣一個“上下文”的存在。比如之前外部變量的引入、全局變量、函數的定義、已經分配的資源等等。有了這些信息,我們才能準確的執行每一句代碼。
同理,JSContext就是JS語言的執行環境,所有JS代碼的執行必須在一個JSContext之中,在WebView中也是一樣,我們可以通過KVC的方式獲取當時WebView的JSContext。通過JSContext運行一段JS代碼十分簡單,如下面這個例子:
JSContext *context = [[JSContext alloc] init]; [context evaluateScript:@"var a = 1;var b = 2;"]; NSInteger sum = [[context evaluateScript:@"a + b"] toInt32];//sum=3
借助evaluateScript API,我們就可以在OC中搭配JSContext執行JS代碼。它的返回值是JS中最后生成的一個值,用屬于當前JSContext中的JSValue(下一節會有介紹)包裹返回。
我們還可以通過KVC的方式,給JSContext塞進去很多全局對象或者全局函數:
JSContext *context = [[JSContext alloc] init]; context[@"globalFunc"] = ^() { NSArray *args = [JSContext currentArguments]; for (id obj in args) { NSLog(@"拿到了參數:%@", obj); } }; context[@"globalProp"] = @"全局變量字符串"; [context evaluateScript:@"globalFunc(globalProp)"];//console輸出:“拿到了參數:全局變量字符串”
這是一個很好用而且很重要的特性,有很多著名的借助JSCore的框架如JSPatch,都利用了這個特性去實現一些很巧妙的事情。在這里我們不過多探討可以利用它做什么,而是去研究它究竟是怎樣運作的。在JSContext的API中,有一個值得注意的只讀屬性 -- JSValue類型的globalObject。它返回當前執行JSContext的全局對象,例如在WebKit中,JSContext就會返回當前的Window對象。
而這個全局對象其實也是JSContext最核心的東西,當我們通過KVC方式與JSContext進去取值賦值的時候,實際上都是在跟這個全局對象做交互,幾乎所有的東西都在全局對象里,可以說,JSContext只是globalObject的一層殼。對于上述兩個例子,本文取了context的globalObject,并轉成了OC對象,如下圖:
可以看到這個globalObject保存了所有的變量與函數,這更加印證了上文的說法(至于為什么globalObject對應OC對象是NSDictionary類型,我們將在下節中講述)。所以我們還能得出另外一個結論,JS中所謂的全局變量,全局函數不過是全局對象的屬性和函數。
同時值得注意的是,每個JSContext都從屬于一個JSVM。我們可以通過JSContext的只讀屬性virtualMachine獲得當前JSContext綁定的JSVM。JSContext和JSVM是多對一的關系,一個JSContext只能綁定一個JSVM,但是一個JSVM可以同時持有多個JSContext。而上文中我們提到,每個JSVM同時只有整個一個線程來執行JS代碼,所以綜合來看,一次簡單的通過JSCore運行JS代碼,并在Native層獲取返回值的過程大致如下:
JSValue
JSValue實例是一個指向JS值的引用指針。我們可以使用JSValue類,在OC和JS的基礎數據類型之間相互轉換。同時我們也可以使用這個類,去創建包裝了Native自定義類的JS對象,或者是那些由Native方法或者Block提供實現JS方法的JS對象。
在JSContext一節中,我們接觸了大量的JSValue類型的變量。在JSContext一節中我們了解到,我們可以很簡單的通過KVC操作JS全局對象,也可以直接獲得JS代碼執行結果的返回值(同時每一個JS中的值都存在于一個執行環境之中,也就是說每個JSValue都存在于一個JSContext之中,這也就是JSValue的作用域),都是因為JSCore幫我們用JSValue在底層自動做了OC和JS的類型轉換。
JSCore一共提供了如下10種類型互換:
Objective-C type | JavaScript type --------------------+--------------------- nil | undefined NSNull | null NSString | string NSNumber | number, boolean NSDictionary | Object object NSArray | Array object NSDate | Date object NSBlock | Function object id | Wrapper object Class | Constructor object
同時還提供了對應的互換API(節選):
+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context; + (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context; - (NSArray *)toArray; - (NSDictionary *)toDictionary;
在講類型轉換前,我們先了解一下JS這門語言的變量類型。根據ECMAScript(可以理解為JS的標準)的定義:JS中存在兩種數據類型的值,一種是基本類型值,它指的是簡單的數據段。第二種是引用類型值,指那些可能由多個值構成的對象。基本類型值包括"undefined","nul","Boolean","Number","String"(是的,String也是基礎類型),除此之外都是引用類型。對于前五種基礎類型的互換,應該沒有太多要講的。接下來會重點講講引用類型的互換:
NSDictionary <--> Object
在上節中,我們把JSContext的globalObject轉換成OC對象,發現是NSDictionary類型。要搞清楚這個轉換,首先我們對JS這門語言面向對象的特性進行一個簡單的了解。在JS中,對象就是一個引用類型的實例。與我們熟悉的OC、Java不一樣,對象并不是一個類的實例,因為在JS中并不存在類的概念。ECMA把對象定義為:無序屬性的集合,其屬性可以包含基本值、對象或者函數。從這個定義我們可以發現,JS中的對象就是無序的鍵值對,這和OC中的NSDictionary,Java中的HashMap何其相似。
var person = { name: "Nicholas",age: 17};//JS中的person對象 NSDictionary *person = @{@"name":@"Nicholas",@"age":@17};//OC中的person dictionary
在上面的實例代碼中,筆者使用了類似的方式創建了JS中的對象(在JS中叫“對象字面量”表示法)與OC中的NSDictionary,相信可以更有助理解這兩個轉換。
NSBlock <--> Function Object
在上節的例子中,筆者在JSContext賦值了一個"globalFunc"的Block,并可以在JS代碼中當成一個函數直接調用。我還可以使用"typeof"關鍵字來判斷globalFunc在JS中的類型:
NSString *type = [[context evaluateScript:@"typeof globalFunc"] toString];//type的值為"function"
通過這個例子,我們也能發現傳入的Block對象在JS中已經被轉成了"function"類型。"Function Object"這個概念對于我們寫慣傳統面向對象語言的開發者來說,可能會比較晦澀。而實際上,JS這門語言,除了基本類型以外,就是引用類型。函數實際上也是一個"Function"類型的對象,每個函數名實則是指向一個函數對象的引用。比如我們可以這樣在JS中定義一個函數:
var sum = function(num1,num2){ return num1 + num2; }
同時我們還可以這樣定義一個函數(不推薦):
var sum = new Function("num1","num2","return num1 + num2");
按照第二種寫法,我們就能很直觀的理解到函數也是對象,它的構造函數就是Function,函數名只是指向這個對象的指針。而NSBlock是一個包裹了函數指針的類,JSCore把Function Object轉成NSBlock對象,可以說是很合適的。
JSExport
實現JSExport協議可以開放OC類和它們的實例方法,類方法,以及屬性給JS調用。
除了上一節提到的幾種特殊類型的轉換,我們還剩下NSDate類型,與id、class類型的轉換需要弄清楚。而NSDate類型無需贅述,所以我們在這一節重點要弄清楚后兩者的轉換。
而通常情況下,我們如果想在JS環境中使用OC中的類和對象,需要它們實現JSExport協議,來確定暴露給JS環境中的屬性和方法。比如我們需要向JS環境中暴露一個Person的類與獲取名字的方法:
@protocol PersonProtocol <JSExport> - (NSString *)fullName;//fullName用來拼接firstName和lastName,并返回全名 @end @interface JSExportPerson : NSObject <PersonProtocol> - (NSString *)sayFullName;//sayFullName方法 @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; @end
然后,我們可以把一個JSExportPerson的一個實例傳入JSContext,并且可以直接執行fullName方法:
JSExportPerson *person = [[JSExportPerson alloc] init]; context[@"person"] = person; person.firstName = @"Di"; person.lastName =@"Tang"; [context evaluateScript:@"log(person.fullName())"];//調Native方法,打印出person實例的全名 [context evaluateScript:@"person.sayFullName())"];//提示TypeError,'person.sayFullName' is undefined
這就是一個很簡單的使用JSExport的例子,但請注意,我們只能調用在該對象在JSExport中開放出去的方法,如果并未開放出去,如上例中的"sayFullName"方法,直接調用則會報TypeError錯誤,因為該方法在JS環境中并未被定義。
講完JSExport的具體使用方法,我們來看看我們最開始的問題。當一個OC對象傳入JS環境之后,會轉成一個JSWrapperObject。那問題來了,什么是JSWrapperObject?在JSCore的源碼中,我們可以找到一些線索。首先在JSCore的JSValue中,我們可以發現這樣一個方法:
@method @abstract Create a JSValue by converting an Objective-C object. @discussion The resulting JSValue retains the provided Objective-C object. @param value The Objective-C object to be converted. @result The new JSValue. */ + (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context;
這個API可以傳入任意一個類型的OC對象,然后返回一個持有該OC對象的JSValue。那這個過程肯定涉及到OC對象到JS對象的互換,所以我們只要分析一下這個方法的源碼(基于 這個分支 進行分析)。由于源碼實現過長,我們只需要關注核心代碼,在JSContext中有一個"wrapperForObjCObject"方法,而實際上它又是調用了JSWrapperMap的"jsWrapperForObject"方法,這個方法就可以解答所有的疑惑:
//接受一個入參object,并返回一個JSValue - (JSValue *)jsWrapperForObject:(id)object { //對于每個對象,有專門的jsWrapper JSC::JSObject* jsWrapper = m_cachedJSWrappers.get(object); if (jsWrapper) return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context]; JSValue *wrapper; //如果該對象是個類對象,則會直接拿到classInfo的constructor為實際的Value if (class_isMetaClass(object_getClass(object))) wrapper = [[self classInfoForClass:(Class)object] constructor]; else { //對于普通的實例對象,由對應的classInfo負責生成相應JSWrappper同時retain對應的OC對象,并設置相應的Prototype JSObjCClassInfo* classInfo = [self classInfoForClass:[object class]]; wrapper = [classInfo wrapperForObject:object]; } JSC::ExecState* exec = toJS([m_context JSGlobalContextRef]); //將wrapper的值寫入JS環境 jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec); //緩存object的wrapper對象 m_cachedJSWrappers.set(object, jsWrapper); return wrapper; }
在我們創建"JSWrapperObject"的對象過程中,我們會通過JSWrapperMap來為每個傳入的對象創建對應的JSObjCClassInfo。這是一個非常重要的類,它有這個類對應JS對象的原型(Prototype)與構造函數(Constructor)。然后由JSObjCClassInfo去生成具體OC對象的JSWrapper對象,這個JSWrapper對象中就有一個JS對象所需要的所有信息(即Prototype和Constructor)以及對應OC對象的指針。之后,把這個jsWrapper對象寫入JS環境中,即可在JS環境中使用這個對象了。
這也就是"JSWrapperObject"的真面目。而我們上文中提到,如果傳入的是類,那么在JS環境中會生成constructor對象,那么這點也很容易從源碼中看到,當檢測到傳入的是類的時候(類本身也是個對象),則會直接返回constructor屬性,這也就是"constructor object"的真面目,實際上就是一個構造函數。
那現在還有兩個問題,第一個問題是,OC對象有自己的繼承關系,那么在JS環境中如何描述這個繼承關系?第二個問題是,JSExport的方法和屬性,又是如何讓JS環境中調用的呢?
我們先看第一個問題,繼承關系要如何解決?在JS中,繼承是通過原型鏈來實現,那什么是原型呢?原型對象是一個普通對象,而且就是構造函數的一個實例。所有通過該構造函數生成的對象都共享這一個對象,當查找某個對象的屬性值,結果不存在時,這時就會去對象的原型對象繼續找尋,是否存在該屬性,這樣就達到了一個封裝的目的。我們通過一個Person原型對象快速了解:
//原型對象是一個普通對象,而且就是Person構造函數的一個實例。所有Person構造函數的實例都共享這一個原型對象。 Person.prototype = { name: 'tony stark', age: 48, job: 'Iron Man', sayName: function() { alert(this.name); } }
而原型鏈就是JS中實現繼承的關鍵,它的本質就是重寫構造函數的原型對象,鏈接另一個構造函數的原型對象。這樣查找某個對象的屬性,會沿著這條原型鏈一直查找下去,從而達到繼承的目的。我們通過一個例子快速了解一下:
function mammal (){} mammal.prototype.commonness = function(){ alert('哺乳動物都用肺呼吸'); }; function Person() {} Person.prototype = new mammal();//原型鏈的生成,Person的實例也可以訪問commonness屬性了 Person.prototype.name = 'tony stark'; Person.prototype.age = 48; Person.prototype.job = 'Iron Man'; Person.prototype.sayName = function() { alert(this.name); } var person1 = new Person(); person1.commonness(); // 彈出'哺乳動物都用肺呼吸' person1.sayName(); // 'tony stark'
而我們在生成對象的classinfo的時候(具體代碼見"allocateConstructorAndPrototypeWithSuperClassInfo"),還會生成父類的classInfo。對每個實現過JSExport的OC類,JSContext里都會提供一個prototype。比如NSObject類,在JS里面就會有對應的Object Prototype。對于其它的OC類,會創建對應的Prototype,這個prototype的內部屬性[Prototype]會指向為這個OC類的父類創建的Prototype。這個JS原型鏈就能反應出對應OC類的繼承關系,在上例中,Person.prototype被賦值為一個mammal的實例對象,即原型的鏈接過程。
講完第一個問題,我們再來看看第二個問題。那JSExport是如何暴露OC方法到JS環境的呢?這個問題的答案同樣出現在我們生成對象的classInfo的時候:
Protocol *exportProtocol = getJSExportProtocol(); forEachProtocolImplementingProtocol(m_class, exportProtocol, ^(Protocol *protocol){ copyPrototypeProperties(m_context, m_class, protocol, prototype); copyMethodsToObject(m_context, m_class, protocol, NO, constructor); });
對于每個聲明在JSExport里的屬性和方法,classInfo會在prototype和constructor里面存入對應的property和method。之后我們就可以通過具體的methodName和PropertyName生成的setter和getter方法,來獲取實際的SEL。最后就可以讓JSExport中的方法和屬性得到正確的訪問。所以簡單點講,JSExport就是負責把這些方法打個標,以methodName為key,SEL為value,存入一個map(prototype和constructor本質上就是一個Map)中去,之后就可以通過methodName拿到對應的SEL進行調用。這也就解釋了上例中,我們調用一個沒有在JSExport中開放的方法會顯示undefined,因為生成的對象里根本沒有這個key。
總結
JSCore給iOS App提供了JS可以解釋執行的運行環境與資源。對于我們實際開發而言,最主要的就是JSContext和JSValue這兩個類。JSContext提供互相調用的接口,JSValue為這個互相調用提供數據類型的橋接轉換。讓JS可以執行Native方法,并讓Native回調JS,反之亦然。
利用JSCore,我們可以做很多有想象空間的事。所有基于JSCore的Hybrid開發基本就是靠上圖的原理來實現互相調用,區別只是具體的實現方式和用途不大相同。大道至簡,只要正確理解這個基本流程,其它的所有方案不過是一些變通,都可以很快掌握。
一些引申閱讀
JSPatch的對象和方法沒有實現JSExport協議,JS是如何調OC方法的?
JS調OC并不是通過JSExport。通過JSExport實現的方式有諸多問題,我們需要先寫好Native的類,并實現JSExport協議,這個本身就不能滿足“Patch”的需求。
所以JSPatch另辟蹊徑,使用了OC的Runtime消息轉發機制做這個事情,如下面這一個簡單的JSPatch調用代碼:
require('UIView') var view = UIView.alloc().init()
require在全局作用域里生成UIView變量,來表示這個對象是一個OCClass。
通過正則把.alloc()改成._c('alloc'),來進行方法收口,最終會調用_methodFunc()把類名、對象、MethodName通過在Context早已定義好的Native方法,傳給OC環境。
最終調用OC的CallSelector方法,底層通過從JS環境拿到的類名、方法名、對象之后,通過NSInvocation實現動態調用。
JSPatch的通信并沒有通過JSExport協議,而是借助JSCore的Context與JSCore的類型轉換和OC的消息轉發機制來完成動態調用,實現思路真的很巧妙。
橋方法的實現是怎么通過JSCore交互的?
市面上常見的橋方法調用有兩種:
通過UIWebView的delegate方法:shouldStartLoadWithRequest來處理橋接JS請求。JSRequest會帶上methodName,通過WebViewBridge類調用該method。執行完之后,會使用WebView來執行JS的回調方法,當然實際上也是調用的WebView中的JSContext來執行JS,完成整個調用回調流程。
通過UIWebView的delegate方法:在webViewDidFinishLoadwebViewDidFinishLoad里通過KVC的方式獲取UIWebView的JSContext,然后通過這個JSContext設置已經準備好的橋方法供JS環境調用。
作者簡介
唐笛,美團點評高級工程師。2017年加入原美團,目前作為外賣iOS團隊主力開發,主要負責移動端基礎設施建設,動態化等方向相關推進工作,致力于提升移動端研發效率與研發質量。
【本文轉載自美團技術團隊微信公眾號,原文鏈接:https://mp.weixin.qq.com/s/H5wBNAm93uPJDvCQCg0_cg】
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。