JavaScriptCore全面解析

 導語JavaScript愈來愈多地出如今咱們客戶端開發的視野中,從ReactNative到JSpatch,JavaScript與客戶端相結合的技術開始變得魅力無窮。本文主要講解iOS中的JavaScriptCore框架,正是它爲iOS提供了執行JavaScript代碼的能力。將來的技術突飛猛進,JavaScript與iOS正在碰撞出新的激情。前端


做者殷源--騰訊移動端工程師程序員

@IMWeb前端社區web


JavaScript愈來愈多地出如今咱們客戶端開發的視野中,從ReactNative到JSpatch,JavaScript與客戶端相結合的技術開始變得魅力無窮。本文主要講解iOS中的JavaScriptCore框架,正是它爲iOS提供了執行JavaScript代碼的能力。將來的技術突飛猛進,JavaScript與iOS正在碰撞出新的激情。chrome

JavaScriptCore是JavaScript的虛擬機,爲JavaScript的執行提供底層資源。編程

1、JavaScript

在討論JavaScriptCore以前,咱們首先必須對JavaScript有所瞭解。api

1. JavaScript幹啥的?

說的高大上一點:一門基於原型、函數先行的高級編程語言,經過解釋執行,是動態類型的直譯語言。是一門多範式的語言,它支持面向對象編程,命令式編程,以及函數式編程。數組

說的通俗一點:主要用於網頁,爲其提供動態交互的能力。可嵌入動態文本於HTML頁面,對瀏覽器事件做出響應,讀寫HTML元素,控制cookies等。瀏覽器

再通俗一點:搶月餅,button.click()。(PS:請謹慎使用while循環)安全

  • 圖片

2. JavaScript起源與歷史

1990年末,歐洲核能研究組織(CERN)科學家Tim Berners-Lee,在互聯網的基礎上,發明了萬維網(World Wide Web),今後能夠在網上瀏覽網頁文件。cookie

1994年12月,Netscape 發佈了一款面向普通用戶的新一代的瀏覽器Navigator 1.0版,市場份額一舉超過90%。

1995年,Netscape公司僱傭了程序員Brendan Eich開發這種嵌入網頁的腳本語言。最初名字叫作Mocha,1995年9月改成LiveScript。

1995年12月,Netscape公司與Sun公司達成協議,後者容許將這種語言叫作JavaScript。

3. JavaScript與ECMAScript

「JavaScript」是Sun公司的註冊商標,用來特製網景(如今的Mozilla)對於這門語言的實現。網景將這門語言做爲標準提交給了ECMA——歐洲計算機制造協會。因爲商標上的衝突,這門語言的標準版本改了一個醜陋的名字「ECMAScript」。一樣因爲商標的衝突,微軟對這門語言的實現版本取了一個廣爲人知的名字「Jscript」。

ECMAScript做爲JavaScript的標準,通常認爲後者是前者的實現。

4. Java和JavaScript

Java 和 JavaScript 是兩門不一樣的編程語言 
通常認爲,當時 Netscape 之因此將 LiveScript 命名爲 JavaScript,是由於 Java 是當時最流行的編程語言,帶有 「Java」 的名字有助於這門新生語言的傳播。

2、 JavaScriptCore

1. 瀏覽器演進

演進完整圖
https://upload.wikimedia.org/wikipedia/commons/7/74/Timeline_of_web_browsers.svg

WebKit分支
如今使用WebKit的主要兩個瀏覽器Sfari和Chromium(Chorme的開源項目)。
WebKit起源於KDE的開源項目Konqueror的分支,由蘋果公司用於Sfari瀏覽器。
其一條分支發展成爲Chorme的內核,2013年Google在此基礎上開發了新的Blink內核。

2. WebKit排版引擎

webkit是sfari、chrome等瀏覽器的排版引擎,各部分架構圖以下

webkit Embedding API是browser UI與webpage進行交互的api接口;

platformAPI提供與底層驅動的交互, 如網絡, 字體渲染, 影音文件解碼, 渲染引擎等;

WebCore它實現了對文檔的模型化,包括了CSS, DOM, Render等的實現;

JSCore是專門處理JavaScript腳本的引擎;

3. JavaScript引擎

JavaScript引擎是專門處理JavaScript腳本的虛擬機,通常會附帶在網頁瀏覽器之中。

第一個JavaScript引擎由布蘭登·艾克在網景公司開發,用於Netscape Navigator網頁瀏覽器中。

JavaScriptCore就是一個JavaScript引擎。

下圖是當前主要的還在開發中的JavaScript引擎

4. JavaScriptCore組成

JavaScriptCore主要由如下模塊組成:


Lexer 詞法分析器,將腳本源碼分解成一系列的Token

Parser 語法分析器,處理Token並生成相應的語法樹

LLInt 低級解釋器,執行Parser生成的二進制代碼

Baseline JIT 基線JIT(just in time 實施編譯)

DFG 低延遲優化的JIT

FTL 高通量優化的JIT

5. JavaScriptCore

JavaScriptCore是一個C++實現的開源項目。使用Apple提供的JavaScriptCore框架,你能夠在Objective-C或者基於C的程序中執行Javascript代碼,也能夠向JavaScript環境中插入一些自定義的對象。JavaScriptCore從iOS 7.0以後能夠直接使用。

在JavaScriptCore.h中,咱們能夠看到這個

這裏已經很清晰地列出了JavaScriptCore的主要幾個類:

JSContext
JSValue
JSManagedValue
JSVirtualMachine
JSExport

接下來咱們會依次講解這幾個類的用法。

6. Hello World!

這段代碼展現瞭如何在Objective-C中執行一段JavaScript代碼,而且獲取返回值並轉換成OC數據打印

Output

3、 JSVirtualMachine

一個JSVirtualMachine的實例就是一個完整獨立的JavaScript的執行環境,爲JavaScript的執行提供底層資源。

這個類主要用來作兩件事情:

一、實現併發的JavaScript執行

二、JavaScript和Objective-C橋接對象的內存管理


看下頭文件SVirtualMachine.h裏有什麼:

每個JavaScript上下文(JSContext對象)都歸屬於一個虛擬機(JSVirtualMachine)。每一個虛擬機能夠包含多個不一樣的上下文,並容許在這些不一樣的上下文之間傳值(JSValue對象)。

然而,每一個虛擬機都是完整且獨立的,有其獨立的堆空間和垃圾回收器(garbage collector ),GC沒法處理別的虛擬機堆中的對象,所以你不能把一個虛擬機中建立的值傳給另外一個虛擬機。

線程和JavaScript的併發執行

JavaScriptCore API都是線程安全的。你能夠在任意線程建立JSValue或者執行JS代碼,然而,全部其餘想要使用該虛擬機的線程都要等待。

若是想併發執行JS,須要使用多個不一樣的虛擬機來實現。

能夠在子線程中執行JS代碼。

經過下面這個demo來理解一下這個併發機制

context和context2屬於同一個虛擬機。
context1屬於另外一個虛擬機。
三個線程分別異步執行每秒1次的js log,首先會休眠1秒。
在context上執行一個休眠5秒的JS函數。
首先執行的應該是休眠5秒的JS函數,在此期間,context所處的虛擬機上的其餘調用都會處於等待狀態,所以tick和tick_2在前5秒都不會有執行。
而context1所處的虛擬機仍然能夠正常執行tick_1。
休眠5秒結束後,tick和tick_2纔會開始執行(不保證前後順序)。
實際運行輸出的log是:

4、 JSContext

一個JSContext對象表明一個JavaScript執行環境。在native代碼中,使用JSContext去執行JS代碼,訪問JS中定義或者計算的值,並使JavaScript能夠訪問native的對象、方法、函數。

1. JSContext執行JS代碼

調用evaluateScript函數能夠執行一段top-level 的JS代碼,並可向global對象添加函數和對象定義

其返回值是JavaScript代碼中最後一個生成的值

API Reference

2. JSContext訪問JS對象

一個JSContext對象對應了一個全局對象(global object)。例如web瀏覽器中中的JSContext,其全局對象就是window對象。在其餘環境中,全局對象也承擔了相似的角色,用來區分不一樣的JavaScript context的做用域。全局變量是全局對象的屬性,能夠經過JSValue對象或者context下標的方式來訪問。

一言不合上代碼:

Output:

這裏列出了三種訪問JavaScript對象的方法

經過context的實例方法objectForKeyedSubscript

經過context.globalObject的objectForKeyedSubscript實例方法

經過下標方式

設置屬性也是對應的。

API Reference

5、 JSValue

一個JSValue實例就是一個JavaScript值的引用。使用JSValue類在JavaScript和native代碼之間轉換一些基本類型的數據(好比數值和字符串)。你也可使用這個類去建立包裝了自定義類的native對象的JavaScript對象,或者建立由native方法或者block實現的JavaScript函數。

每一個JSValue實例都來源於一個表明JavaScript執行環境的JSContext對象,這個執行環境就包含了這個JSValue對應的值。每一個JSValue對象都持有其JSContext對象的強引用,只要有任何一個與特定JSContext關聯的JSValue被持有(retain),這個JSContext就會一直存活。經過調用JSValue的實例方法返回的其餘的JSValue對象都屬於與最始的JSValue相同的JSContext。

每一個JSValue都經過其JSContext間接關聯了一個特定的表明執行資源基礎的JSVirtualMachine對象。你只能將一個JSValue對象傳給由相同虛擬機管理(host)的JSValue或者JSContext的實例方法。若是嘗試把一個虛擬機的JSValue傳給另外一個虛擬機,將會觸發一個Objective-C異常。

1. JSValue類型轉換

JSValue提供了一系列的方法將native與JavaScript的數據類型進行相互轉換:

2. NSDictionary與JS對象

NSDictionary對象以及其包含的keys與JavaScript中的對應名稱的屬性相互轉換。key所對應的值也會遞歸地進行拷貝和轉換。

Output:

可見,JS中的對象能夠直接轉換成Objective-C中的NSDictionary,NSDictionary傳入JavaScript也能夠直接看成對象被使用。

3. NSArray與JS數組

NSArray對象與JavaScript中的array相互轉轉。其子元素也會遞歸地進行拷貝和轉換。

Output:

4. Block/函數和JS function

Objective-C中的block轉換成JavaScript中的function對象。參數以及返回類型使用相同的規則轉換。

將一個表明native的block或者方法的JavaScript function進行轉換將會獲得那個block或方法。
其餘的JavaScript函數將會被轉換爲一個空的dictionary。由於JavaScript函數也是一個對象。

5. OC對象和JS對象

對於全部其餘native的對象類型,JavaScriptCore都會建立一個擁有constructor原型鏈的wrapper對象,用來反映native類型的繼承關係。默認狀況下,native對象的屬性和方法並不會導出給其對應的JavaScript wrapper對象。經過JSExport協議可選擇性地導出屬性和方法。

後面會詳細講解對象類型的轉換。

6、 JSExport

JSExport協議提供了一種聲明式的方法去向JavaScript代碼導出Objective-C的實例類及其實例方法,類方法和屬性。

1. 在JavaScript中調用native代碼

兩種方式:

一、Block

二、JSExport

Block的方式很簡單,以下:

Output:

JSExport的方式須要經過繼承JSExport協議的方式來導出指定的方法和屬性:

繼承於JSExport協議的MyPointExports協議中的實例變量,實例方法和類方法都會被導出,而MyPoint類的- (void)myPrivateMethod方法卻不會被導出。
在OC代碼中咱們這樣導出:

在JS代碼中能夠這樣調用:

2. 導出OC方法和屬性給JS

默認狀況下,一個Objective-C類的方法和屬性是不會導出給JavaScript的。你必須選擇指定的方法和屬性來導出。對於一個class實現的每一個協議,若是這個協議繼承了JSExport協議,JavaScriptCore就將這個協議的方法和屬性列表導出給JavaScript。

對於每個導出的實例方法,JavaScriptCore都會在prototype中建立一個存取器屬性。對於每個導出的類方法,JavaScriptCore會在constructor對象中建立一個對應的JavaScript function。

在Objective-C中經過@property聲明的屬性決定了JavaScript中的對應屬性的特徵:

Objective-C類中的屬性,成員變量以及返回值都將根據JSValue指定的拷貝協議進行轉換。


3. 函數名轉換

轉換成駝峯形式:

去掉全部的冒號

全部冒號後的第一個小寫字母都會被轉爲大寫

4. 自定義導出函數名

若是不喜歡默認的轉換規則,也可使用JSExportAs來自定義轉換

5. 導出OC對象給JS

如何導出自定義的對象?

自定義對象有複雜的繼承關係是如何導出的?

在討論這個話題以前,咱們首先須要對JavaScript中的對象與繼承關係有所瞭解。

7、 JavaScript對象繼承

若是你已經瞭解JavaScript的對象繼承,能夠跳過本節。

這裏會快速介紹JavaScript對象繼承的一些知識:

1. JavaScript的數據類型

最新的 ECMAScript 標準定義了 7 種數據類型:

6 種 原始類型:

Boolean

Null

Undefined

Number

String

Symbol (ECMAScript 6 新定義)

和 Object

2. JavaScript原始值

除 Object 之外的全部類型都是不可變的(值自己沒法被改變)。咱們稱這些類型的值爲「原始值」。

布爾類型:兩個值:true 和 false

Null 類型:只有一個值: null

Undefined 類型:一個沒有被賦值的變量會有個默認值 undefined

數字類型

字符串類型:不一樣於類 C 語言,JavaScript 字符串是不可更改的。這意味着字符串一旦被建立,就不能被修改

符號類型

3. JavaScript對象

在 Javascript 裏,對象能夠被看做是一組屬性的集合。這些屬性還能夠被增減。屬性的值能夠是任意類型,包括具備複雜數據結構的對象。

如下代碼構造了一個point對象:

圖片

4. JavaScript屬性

ECMAScript定義的對象中有兩種屬性:數據屬性和訪問器屬性。

數據屬性
數據屬性是鍵值對,而且每一個數據屬性擁有下列特性:

訪問器屬性
訪問器屬性有一個或兩個訪問器函數 (get 和 set) 來存取數值,而且有如下特性:

5. JavaScript屬性設置與檢測

設置一個對象的屬性會只會修改或新增其自有屬性,不會改變其繼承的同名屬性

調用一個對象的屬性會依次檢索自己及其繼承的屬性,直到檢測到

Output:

在chrome的控制檯中,咱們分別打印設置x屬性先後point對象的內部結構:

可見,設置一個對象的屬性並不會修改其繼承的屬性,只會修改或增長其自有屬性。

這裏咱們談到了proto和繼承屬性,下面咱們詳細講解。

8、 Prototype

JavaScript對於有基於類的語言經驗的開發人員來講有點使人困惑 (如Java或C ++) ,由於它是動態的,而且自己不提供類實現。(在ES2015/ES6中引入了class關鍵字,可是隻是語法糖,JavaScript 仍然是基於原型的)。

當談到繼承時,Javascript 只有一種結構:對象。每一個對象都有一個內部連接到另外一個對象,稱爲它的原型 prototype。該原型對象有本身的原型,等等,直到達到一個以null爲原型的對象。根據定義,null沒有原型,而且做爲這個原型鏈 prototype chain中的最終連接。

任何一個對象都有一個__proto__屬性,用來表示其繼承了什麼原型。

如下代碼定一個具備繼承關係的對象,point對象繼承了一個具備x,y屬性的原型對象。

在Chrome的控制檯中,咱們打印對象結構:

可見繼承關係,point繼承的原型又繼承了Object.prototype,而Object.prototype的__proto__指向null,於是它是繼承關係的終點。
這裏咱們首先要知道prototype和__proto__是兩種屬性,前者只有function纔有,後者全部的對象都有。後面會詳細講到。

1. JavaScript類?

Javascript 只有一種結構:對象。類的概念又從何而來?

在JavaScript中咱們能夠經過function來模擬類,例如咱們定義一個MyPoint的函數,並把他認做MyPoint類,就能夠經過new來建立具備x,y屬性的對象

打印point對象結構:

這裏出現一個constructor的概念

2. JavaScript constructor

每一個JavaScript函數都自動擁有一個prototype的屬性,這個prototype屬性是一個對象,這個對象包含惟一一個不可枚舉屬性constructor。constructor屬性值是一個函數對象

執行如下代碼咱們會發現對於任意函數 F.prototype.constructor == F

這裏即存在一個反向引用的關係:

3. new發生了什麼?

當調用new MyPoint(99, 66)時,虛擬機生成了一個point對象,並調用了MyPoint的prototype的constructor對象對point進行初始化,而且自動將MyPoint.prototype做爲新對象point的原型。
至關於下面的僞代碼

4. __proto__與prototype

簡單地說:

__proto__是全部對象的屬性,表示對象本身繼承了什麼對象

prototype是Function的屬性,決定了new出來的新對象的__proto__

如圖詳細解釋了二者的區別

5. 打印JavaScript對象結構

在瀏覽器提供的JavaScript調試工具中,咱們能夠很方便地打印出JavaScript對象的內部結構

在Mac/iOS客戶端JavaScriptCore中並無這樣的打印函數,這裏我自定義了一個打印函數

鑑於對象的內部結構容易出現循環引用致使迭代打印陷入死循環,咱們在這裏簡單地處理,對屬性不進行迭代打印。

爲了描述對象的原型鏈,這裏手動在對象末尾對其原型進行打印。

6. log

咱們爲全部的context都添加一個log函數,方便咱們在JS中向控制檯輸出日誌

9、 導出OC對象給JS

如今咱們繼續回到Objective-C中,看下OC對象是如何導出的

1. 簡單對象的導出

當你從一個未指定拷貝協議的Objective-C實例建立一個JavaScript對象時,JavaScriptCore會建立一個JavaScript的wrapper對象。對於具體類型,JavaScriptCore會自動拷貝值到合適的JavaScript類型。

如下代碼定義了一個繼承自NSObject的簡單類

導出對象

而後咱們打印JavaScript中的d_point對象結構以下:

可見,其type屬性並無被導出。
JS中的對象原型是就是Object.prototype。

2. 繼承關係的導出

在JavaScript中,繼承關係是經過原型鏈(prototype chain)來支持的。對於每個導出的Objective-C類,JavaScriptCore會在context中建立一個prototype。對於NSObject類,其prototype對象就是JavaScript context的Object.prototype。對於全部其餘的Objective-C類,JavaScriptCore會建立一個prototype屬性指向其父類的原型屬性的原型對象。如此,JavaScript中的wrapper對象的原型鏈就反映了Objective-C中類型的繼承關係。

咱們讓DPoint繼承子MyPoint

在OC中,它的繼承關係是這樣的

在JS中,它的繼承關係是這樣的

打印對象結構來驗證:

Output:

圖片

可見,DPoint自身的未導出的屬性type沒有在JS對象中反應出來,其繼承的MyPoint的導出的屬性和函數都在JS對象的原型中。

10、 內存管理

1. 循環引用

以前已經講到, 每一個JSValue對象都持有其JSContext對象的強引用,只要有任何一個與特定JSContext關聯的JSValue被持有(retain),這個JSContext就會一直存活。若是咱們將一個native對象導出給JavaScript,即將這個對象交由JavaScript的全局對象持有,引用關係是這樣的:

圖片

這時若是咱們在native對象中強引用持有JSContext或者JSValue,便會形成循環引用:

圖片

所以在使用時要注意如下幾點:

2. 避免直接使用外部context

避免在導出的block/native函數中直接使用JSContext

使用 [JSContext currentContext] 來獲取當前context可以避免循環引用

圖片

3. 避免直接使用外部JSValue

避免在導出的block/native函數中直接使用JSValue

圖片

這裏咱們使用了JSManagedValue來解決這個問題

11、 JSManagedValue

一個JSManagedValue對象包含了一個JSValue對象,「有條件地持有(conditional retain)」的特性使其能夠自動管理內存。

最基本的用法就是用來在導入到JavaScript的native對象中存儲JSValue。

不要在在一個導出到JavaScript的native對象中持有JSValue對象。由於每一個JSValue對象都包含了一個JSContext對象,這種關係將會致使循環引用,於是可能形成內存泄漏。

1. 有條件地持有

所謂「有條件地持有(conditional retain)」,是指在如下兩種狀況任何一個知足的狀況下保證其管理的JSValue被持有:能夠經過JavaScript的對象圖找到該JSValue

能夠經過native對象圖找到該JSManagedValue。使用addManagedReference:withOwner:方法可向虛擬機記錄該關係反之,若是以上條件都不知足,JSManagedValue對象就會將其value置爲nil並釋放該JSValue。

JSManagedValue對其包含的JSValue的持有關係與ARC下的虛引用(weak reference)相似。

2. 爲何不直接用虛引用?

一般咱們使用weak來修飾block內須要使用的外部引用以免循環引用,因爲JSValue對應的JS對象內存由虛擬機進行管理並負責回收,這種方法不能準確地控制block內的引用JSValue的生命週期,可能在block內須要使用JSValue的時候,其已經被虛擬機回收。

API Reference

圖片

12、 異常處理

JSContext的exceptionHandler屬性可用來接收JavaScript中拋出的異常

默認的exceptionHandler會將exception設置給context的exception屬性

所以,默認的表現就是從JavaScript中拋給native的未處理的異常又被拋回到JavaScript中,異常並未被捕獲處理。

將context.exception設置爲nil將會致使JavaScript認爲異常已經被捕獲處理。

圖片

相關文章
相關標籤/搜索