從JSCore瞭解Hybrid開發

CoderSpr1ngHall發表於2019-04-17

Hybrid

前言

最近因為工作的原因,越來越多的動態化開發模式開始在專案中實施。為了對Hybrid的開發有一個深入的瞭解,查閱了相關的部落格和官方文件之後,決定把學到的東西在這裡做一個總結,方便日後查閱。好了,廢話不多說,要研究Hybrid開發,其中必不可少的是要去了解JavaScriptCore(以下簡稱JSCore)。那麼我們就先從 JSCore入手,看看到底是怎麼一個玩法。

引用文件:


什麼是JSCore

JSCoreWebKit預設內嵌的JS引擎。它建立起了Objective-CJavaScript兩門語言溝通的橋樑。iOS7之後,蘋果對WebKit中的JSCore進行了Objective-C的封裝,並提供給所有的iOS開發者。JSCore框架給SwiftOC以及C語言編寫的App提供了呼叫JS程式的能力。同時我們也可以使用JSCore往JS環境中去插入一些自定義物件。JSCore作為蘋果的瀏覽器引擎WebKit中重要組成部分,這個JS引擎已經存在多年。

在業界中流行的動態化開發方案,如React NativeWeex等。其核心模組中必不可少的會用到JSCoreJSCore跟Google自己研發的瀏覽器引擎Chrome的V8一樣,都是為了解釋執行JS的指令碼。

JSCore的四個基本類

JSCore基本類
上圖是蘋果官網JSCore的介紹。從圖中我們可以很清晰的看到,四個主要核心類分別就是:JSContextJSManagedValueJSValueJSVirtualMachine(以下簡稱JSVM)。那麼我們接下來就來分別看看這些類是幹嘛用的。

JSContext

一個JSContext表示了一次JS的執行環境。我們可以通過建立一個JSContext去呼叫JS指令碼,訪問一些JS定義的值和函式,同時也提供了讓JS訪問Native物件,方法的介面。

從字面上面來看,JSContext好像就是“上下文”的意思。那麼什麼是上下文呢?

比如在一篇文章中,我們看到一句話:“他飛快的跑了出去。”但是如果我們不看上下文的話,我們並不知道這句話究竟是什麼意思:誰跑了出去?他是誰?他為什麼要跑? 寫計算機理解的程式語言跟寫文章是相似的,我們執行任何一段語句都需要有這樣一個“上下文”的存在。比如之前外部變數的引入、全域性變數、函式的定義、已經分配的資源等等。有了這些資訊,我們才能準確的執行每一句程式碼。

所以說,JSContext也就是JS的執行環境(也可以說是執行上下文),所有的JS程式碼都必須在一個JSContext中執行。如果我們要在WebView中去獲取JSContext,可以直接通過KVC的方式直接獲取。

    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"var a = 1;var b = 2;"];
    NSInteger sum = [[context evaluateScript:@"a + b"] toInt32];//sum=3
複製程式碼

我們先建立一個JSContext的環境,然後直接通過evaluateScript方法就可以直接執行一段寫好的JS的程式碼。然後返回值是通過JSValue(後面會有介紹)進行包裝後返回。

上面提到了我們要獲取WebView中的JSContext,可以用KVC的方式。同樣的,我們要給JSContext塞全域性物件和全域性函式,也可以使用KVC的方式:

    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輸出:“拿到了引數:全域性變數字串”
複製程式碼

在JSContext的API中,有一個值得注意的只讀屬性 – JSValue型別的globalObject。它返回當前執行JSContext的全域性物件,例如在WebKit中,JSContext就會返回當前的Window物件。而這個全域性物件其實也是JSContext最核心的東西,當我們通過KVC方式與JSContext進去取值賦值的時候,實際上都是在跟這個全域性物件做互動,幾乎所有的東西都在全域性物件裡,可以說,JSContext只是globalObject的一層殼。

JSManagedValue

一個 JSManagedValue 物件是用來包裝一個 JSValue 物件的,JSManagedValue 物件通過新增“有條件的持有(conditional retain)”行為來實現自動記憶體管理。一個managed value 的基本用法就是用來在一個要匯出(exported)到 JavaScript 的 Objective-C 或者 Swift 物件中儲存一個 JavaScript 值。

這裡順便說一下JS的GC機制: JS同樣也不需要我們去手動管理記憶體。JS的記憶體管理使用的是GC機制。不同於OC的引用計數,GC是由GCRoot(context)開始維護的一條引用鏈,一旦引用鏈無法觸達某物件節點,這個物件就會被回收掉。

JSValue

JSValue例項是一個指向JS值的引用指標。我們可以使用JSValue類,在OC和JS的基礎資料型別之間相互轉換。同時我們也可以使用這個類,去建立包裝了Native自定義類的JS物件,或者是那些由Native方法或者Block提供實現JS方法的JS物件。

其實我們從上面的JSContext解釋裡面能看到,每個JSValue都存在於一個JSContext之中,也就是說這個context就是JSValue的作用域。JSCore幫我們用JSValue在底層自動做了一個OC轉JS的型別轉換之後,我們就可以通過JSValue拿到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 Funtion 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;
複製程式碼
NSDictionary <-> Object

上面我們說到JSContext的globalObject可以轉換成OC物件,然後轉成的OC物件是一個NSDictionary型別。其實,在JS中,物件就是一個引用型別的例項,因為JS中並不存在類的概念(ECMA把物件定義為:無序屬性的集合,其屬性可以包含基本值、物件或者函式)。於是我們可以發現JS中的物件就是無序的鍵值對,這就跟NSDictionary相差無幾了。

NSBlock <-> Funtion 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物件,可以說是很合適的。

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++)之間的介面等等。

從功能上來看,一個高階語言虛擬機器主要分為兩部分,一個是直譯器部分,用來執行高階語言編譯生成的ByteCode,還有一部分則是Runtime執行時,用來負責執行時的記憶體空間開闢、管理等等。實際上,JSCore常常被認為是一個JS語言的優化虛擬機器,它做著JVM類似的事情,只是相比靜態編譯的Java,它還多承擔了把JS原始碼編譯成位元組碼的工作。

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

JSExport

實現JSExport協議可以開放OC類和它們的例項方法,類方法,以及屬性給JS呼叫。

如果我們想在JS環境中使用OC中的類和物件,就需要他們實現JSExport協力來確定暴露給JS環境中的屬性和方法。

@protocol PersonProtocol <JSExport>
- (NSString *)stuFullInfo;//stuFullInfo用來拼接stuName和stuID,並返回學生的全部資訊
@end

@interface JSStudent : NSObject <PersonProtocol>
  
- (NSString *)sayStuFullInfo;//sayStuFullInfo方法

@property (nonatomic, copy) NSString *stuName;
@property (nonatomic, copy) NSString *stuID;

@end
複製程式碼

然後我們把JSStudent的一個例項傳入JSContext,並且可以直接執行stuFullInfo方法:

    JSStudent *student = [[JSStudent alloc] init];
    context[@"student"] = student;
    student.stuName = @"LiHeng Xue";
    student.stuID =@"ID0018888";
    [context evaluateScript:@"log(student.stuFullInfoe())"];//調Native方法,列印出student例項的學生的全部資訊
    [context evaluateScript:@"student.sayStuFullInfo())"];//提示TypeError,'student.sayStuFullInfo' is undefined
複製程式碼

在這裡我們就能看得出來了,只有在JSExport裡面開放出去的方法才能夠使用,如果沒有開放出去,如上面的sayStuFullInfo方法,直接呼叫的時候是會報型別錯誤的。

總結一下JSCore

jscore其實就是給APP提供了一個js可以解釋執行的執行環境與資源。我們主要使用的是JSContext和JSValue這兩個類。JSContext提供互相呼叫的介面,JSValue為這個互相呼叫提供資料型別的橋接轉換。讓JS可以執行Native方法,並讓Native回撥JS,反之亦然。


JSCore怎麼實現橋方法

ok,在這裡我們看完了JSCore的一些基本原理,那麼我們就要再來看看JSCore是怎麼實現橋方法的呢?

市面上常見的橋方法呼叫有兩種:

  • 通過UIWebView的delegate方法:shouldStartLoadWithRequest來處理橋接JS請求。JSRequest會帶上methodName,通過WebViewBridge類呼叫該method。執行完之後,會使用WebView來執行JS的回撥方法,當然實際上也是呼叫的WebView中的JSContext來執行JS,完成整個呼叫回撥流程。
  • 通過UIWebView的delegate方法:在webViewDidFinishLoadwebViewDidFinishLoad裡通過KVC的方式獲取UIWebView的JSContext,然後通過這個JSContext設定已經準備好的橋方法供JS環境呼叫。

這裡面使用的最廣泛的就是一個開源庫:WebViewJavaScriptBridge

WebViewJavaScript的解讀,請看我的下一篇帖子

相關文章