總第277篇javascript
2018年 第69篇php
你們好,我是美團技術團隊的程序員鼓勵師美美,今天正式出道,之後多多指教喲~ ~html
噹噹噹當,今天美美給你們特別介紹一個全新的欄目,它的名字是:基——本——功!前端
以前啊咱們公衆號的內容要求可嚴了,要麼技術上有原創性,要麼是程序員小哥哥們本身實踐經驗的總結。但是大家知道嗎,美團技術同窗均可愛學習了,內部Wiki裏積累了好多好多深刻研究和學習性的技術文章。美美一直以爲這些對其餘公司的工程師小哥哥也是好東西,因此就和院長說選出一些能對外分享的,開個新專欄吧。圍着他說啊說啊好半天,他老人家終於贊成啦~~java
今天是「基本功」專欄的第一篇,咱們一塊兒來好好深刻學習一下Apple的框架JavaScriptCore,聽做者唐笛哥哥說,個人iPhone上不少App能高效開發出來,都離不開它的功勞呢。Enjoy Reading!git
動態化做爲移動客戶端技術的一個重要分支,一直是業界積極探索的方向。目前業界流行的動態化方案,如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瀏覽器開始談起。github
JavaScript誕生於1995年,它的設計者是Netscape的Brendan Eich,而此時的Netscape正是瀏覽器市場的霸主。web
而二十多年前,當時人們在瀏覽網頁的體驗極差,由於那會兒的瀏覽器幾乎只有頁面的展現能力,沒有和用戶的交互邏輯處理能力。因此即便一個必填輸入框傳空,也須要通過服務端驗證,等到返回結果以後纔給出響應,再加上當時的網速很慢,可能半分鐘過去了,返回的結果是告訴你某個必填字段未填。因此Brendan花了十天寫出了JavaScript,由瀏覽器解釋執行,今後以後瀏覽器也有了一些基本的交互處理能力,以及表單數據驗證能力。sql
而Brendan可能沒有想到,在二十多年後的今天。JS這門解釋執行的動態腳本語言,不光成爲前端屆的「正統」,還***了後端開發領域,在編程語言排行榜上進入前三甲,僅次於Python和Java。而如何解釋執行JS,則是各家引擎的核心技術。目前市面上比較常見的JS引擎有Google的V8(它被運用在Android操做系統以及Google的Chrome上),以及咱們今天的主角JSCore(它被運用在iOS操做系統以及Safari上)。
咱們天天都會接觸瀏覽器,使用瀏覽器進行工做、娛樂。讓瀏覽器可以正常工做最核心的部分就是瀏覽器的內核,每一個瀏覽器都有本身的內核,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。
在上面的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是WebKit默認內嵌的JS引擎,之因此說是默認內嵌,是由於不少基於WebKit分支開發的瀏覽器引擎都開發了自家的JS引擎,其中最出名的就是Chrome的V8。這些JS引擎的使命都相同,那就是解釋執行JS腳本。而從上面的渲染流程圖咱們能夠看到,JS和DOM樹之間存在着互相關聯,這是由於瀏覽器中的JS腳本最主要的功能就是操做DOM樹,並與之交互。一樣的,咱們也經過一張圖看下它的工做流程:
能夠看到,相比靜態編譯語言生成語法樹以後,還須要進行連接,裝載生成可執行文件等操做,解釋型語言在流程上要簡化不少。這張流程圖右邊畫框的部分就是JSCore的組成部分:Lexer、Parser、LLInt以及JIT的部分(之因此JIT的部分是用橙色標註,是由於並非全部的JSCore中都有JIT部分)。接下來咱們就搭配整個工做流程介紹每一部分,它主要分爲如下三個部分:詞法分析、語法分析以及解釋執行。
PS:嚴格的講,語言自己並不存在編譯型或者是解釋型,由於語言只是一些抽象的定義與約束,並不要求具體的實現,執行方式。這裏講JS是一門「解釋型語言」只是JS通常是被JS引擎動態解釋執行,而並非語言自己的屬性。
詞法分析很好理解,就是把一段咱們寫的源代碼分解成Token序列的過程,這一過程也叫分詞。在JSCore,詞法分析是由Lexer來完成(有的編譯器或者解釋器把分詞叫作Scanner)。
這是一句很簡單的C語言表達式:
sum = 3 + 2;
將其標記化以後能夠獲得下表的內容:
這就是詞法分析以後的結果,可是詞法分析並不會關注每一個Token之間的關係,是否匹配,僅僅是把它們區分開來,等待語法分析來把這些Token「串起來」。詞法分析函數通常是由語法分析器(Parser)來進行調用的。在JSCore中,詞法分析器Lexer的代碼主要集中在parser/Lexer.h、Lexer.cpp中。
跟人類語言同樣,咱們講話的時候實際上是按照約定俗成,交流習慣按照必定的語法講出一個又一個詞語。那類比到計算機語言,計算機要理解一門計算機語言,也要理解一個語句的語法。例如如下一段JS語句:
var sum = 2 + 3;var a = sum + 5;
Parser會把Lexer分析以後生成的token序列進行語法分析,並生成對應的一棵抽象語法樹(AST)。這個樹長什麼樣呢?在這裏推薦一個網站:esprima Parser,輸入JS語句能夠立馬生成咱們所需的AST。例如,以上語句就被生成這樣的一棵樹:
以後,ByteCodeGenerator會根據AST來生成JSCore的字節碼,完成整個語法解析步驟。
JS源代碼通過了詞法分析和語法分析這兩個步驟,轉成了字節碼,其實就是通過任何一門程序語言必經的步驟--編譯。可是不一樣於咱們編譯運行OC代碼,JS編譯結束以後,並不會生成存放在內存或者硬盤之中的目標代碼或可執行文件。生成的指令字節碼,會被當即被JSCore這臺虛擬機進行逐行解釋執行。
運行指令字節碼(ByteCode)是JS引擎中很核心的部分,各家JS引擎的優化也主要集中於此。JSByteCode的解釋執行是一套很複雜的系統,特別是加入了OSR和多級JIT技術以後,整個解釋執行變的愈來愈高效,而且讓整個ByteCode的執行在低延時之間和高吞吐之間有個很好的平衡:由低延時的LLInt來解釋執行ByteCode,當遇到屢次重複調用或者是遞歸,循環等條件會經過OSR切換成JIT進行解釋執行(根據具體觸發條件會進入不一樣的JIT進行動態解釋)來加快速度。因爲這部份內容較爲複雜,並且不是本文重點,故只作簡單介紹,不作深刻的討論。
除了以上部分,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有了多線程異步能力。
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(如下簡稱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一樣也不須要咱們去手動管理內存。JS的內存管理使用的是GC機制(Tracing Garbage Collection)。不一樣於OC的引用計數,Tracing Garbage Collection是由GCRoot(Context)開始維護的一條引用鏈,一旦引用鏈沒法觸達某對象節點,這個對象就會被回收掉。以下圖所示:
一個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實例是一個指向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也是基礎類型),除此以外都是引用類型。對於前五種基礎類型的互換,應該沒有太多要講的。接下來會重點講講引用類型的互換:
在上節中,咱們把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,相信能夠更有助理解這兩個轉換。
在上節的例子中,筆者在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協議能夠開放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開發基本就是靠上圖的原理來實現互相調用,區別只是具體的實現方式和用途不大相同。大道至簡,只要正確理解這個基本流程,其它的全部方案不過是一些變通,均可以很快掌握。
一些引伸閱讀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的消息轉發機制來完成動態調用,實現思路真的很巧妙。
市面上常見的橋方法調用有兩種:
經過UIWebView的delegate方法:shouldStartLoadWithRequest來處理橋接JS請求。JSRequest會帶上methodName,經過WebViewBridge類調用該method。執行完以後,會使用WebView來執行JS的回調方法,固然實際上也是調用的WebView中的JSContext來執行JS,完成整個調用回調流程。
經過UIWebView的delegate方法:在webViewDidFinishLoadwebViewDidFinishLoad裏經過KVC的方式獲取UIWebView的JSContext,而後經過這個JSContext設置已經準備好的橋方法供JS環境調用。
《JavaScript高級程序設計》
虛擬機隨談(一)
唐笛,美團點評高級工程師。2017年加入原美團,目前做爲外賣iOS團隊主力開發,主要負責移動端基礎設施建設,動態化等方向相關推動工做,致力於提高移動端研發效率與研發質量。
---------- END ----------
招聘信息
美團外賣長期招聘Android、iOS、FE 高級/資深工程師和技術專家,base 北京、上海、成都,歡迎有興趣的同窗投遞簡歷到chenhang03#meituan.com。
也許你還想看