經過 JavaScriptCore 框架,你能夠在 Objective-C 或者基於 C 的程序中運行(evaluate) JavaScript 程序。它還能幫你插入一些自定義對象到 JavaScript 環境中去。javascript
JavaScriptCore框架其實就是基於webkit中以C/C++實現的JavaScriptCore的一個包裝,在舊版本iOS開發中,不少開發者也會自行將webkit的庫引入項目編譯使用。如今iOS7把它當成了標準庫。java
JavaScriptCore框架在OS X平臺上很早就存在了,可是接口是純C語言的,而在iOS7以前 蘋果沒有開放此框架,很多須要在iOS app中處理JavaScript的都要從開源的WebKit中編譯出JavaScriptCore.a。以後蘋果爲了方便開發人員將JavaScriptCore框 架開放了,同時還提供了Objective-C的封裝接口。ios
JavaScriptCore 的特性是自動化的、安全的、高保真的。本篇文章將要討論的就是基於Objective-C封裝的JavaScriptCore框架。git
NSObject程序員
NSObject 是大部分 Objective-C 類的根類。github
JSContextweb
一個 JSContext 對象表明一個 JavaScript 執行環境(execution environment),負責原生和 JavaScript 的數據傳遞。經過jSCore執行的JS代碼都得經過JSContext來執行。apache
JSContext對應着一個全局對象,至關於瀏覽器中的window對象,JSContext中有一個GlobalObject屬性,實際上JS代碼都是在這個GlobalObject上執行的,可是爲了容易理解,能夠把JSContext等價於全局對象。數組
JSManagedValue瀏覽器
一個 JSManagedValue 對象包裝了一個 JSValue 對象,JSManagedValue 對象經過添加「有條件的持有(conditional retain)」行爲來實現自動內存管理。
JSValue
一個 JSValue 實例是 一個JavaScript 的值對象,用來記錄 JavaScript的原始值,並提供進行原生值對象轉換的接口方法。
JS中的值不能直接拿到OC中使用,所以JSValue就是對JS值的封裝,這個JS值能夠是JS中的number,boolean等基本類型,也多是對象,函數,甚至能夠是undefined,或者null等。
JSValue 不能獨立存在,只能存在於某一個JSContext中。
JSValue對其對應的JS值和其所屬的JSContext對象都是強引用的關係。由於jSValue須要這兩個東西來執行JS代碼,因此JSValue會一直持有着它們
JSVirtualMachine
一個 JSVirtualMachine 實例表明一個自包含的(self-contained) JavaScript 執行環境(execution environment),爲JavaScript代碼的運行提供一個虛擬機環境。
在同一時間 內,JSVirtualMachine 只能執行一個線程,若是想要多個縣城執行任務,你能夠建立多個JSVirtualMachine。每一個 JSVirtualMachine 都有本身的垃圾回收器,以便進行內存管理,因此多個 JSVirtualMachine 之間的對象沒法傳遞。
JSExport
JSExport 協議提供了一些關於將 Objective-C 實例的類和它們的實例方法,類方法以及屬性轉成 JavaScript 代碼的接口聲明。
首先咱們先用一個圖來表示他們之間的關係:
從圖中能夠看出,一個 JSVirtualMachine 包含多個 JSContext,同一個 JSContext 中 又包含多個 JSValue。這三個類提供的接口可使原生 app 訪問和執行JavaScript函數,也可讓JavaScript 執行原生代碼。接下來咱們用兩段代碼來表示:
//計算從n 到1 全部的數字相乘的結果
var multiply = function(n) {
if (n < 0) {
return;
}
if (n == 0) {
return 1;
}
return n * multiply(n - 1);
};
複製代碼
//從bundle中加載這段JS代碼。
NSString *multiplyScript = [self loadJSFromBundle];
//使用jsvm建立一個JSContext,並用他來執行這段JS代碼,這句的效果就至關於在一個全局對象中聲明瞭一個叫multiply的函數,可是沒有調用它,只是聲明,因此執行完這段JS代碼後沒有返回值。
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:multiplyScript];
//再從這個全局對象中獲取這個函數,這裏咱們用到了一種相似字典的下標寫法來獲取對應的JS函數,就像在一個字典中取這個key對應的value同樣簡單,實際上,JS中的對象就是以 key : Value 的形式存儲屬性的,且JS中的object對象類型,對應到OC中就是字典類型,因此這種寫法天然且合理。
JSValue *function = context[@"multiply"];
//調用callWithArguments方法,便可調用該函數,這個方法接收一個數組爲參數,這是由於JS中的函數的參數都不是固定的,咱們構建了一個數組,並把NSNumber類型的5傳了過去,然而JS確定是不知道什麼是NSNumber的,可是別擔憂,JSCore會幫咱們自動轉換JS中對應的類型, 這裏會把NSNumber類型的5轉成JS中number類型的5,而後再去調用這個函數(這就是前面說的API目標中自動化的體現)。
JSValue *result = [function callWithArguments:@[@5]];
NSLog(@"%d",[result toInt32]);
複製代碼
首先咱們先經過一張圖來解釋他們之間的交互關係:
咱們能夠看到,每個JavaScriptCore 中的 JSVirtualMachine 對應着一個原生線程,同一個JSVirtualMachine 中可使用 JSValue 和原生線程通訊,遵循的是 JSExport 協議:原生線程能夠將類方法和屬性提供給JavaScriptCore 使用,JavaScriptCore 能夠將 JSValue 提供給原生線程使用。
咱們用一段代碼來表示在OC中調用JS,定義一段js代碼 "var i = 4 + 8" 聲明瞭一個變量 i。代碼以下所示:
JSContext *context = [[JSContext alloc] init];
// 解析執行 JavaScript 腳本
JSValue *value = [context evaluateScript:@"var i = 4 + 8"];
// 轉換 i 變量爲原生對象
JSValue *i = value[@"i"];
NSNumber *number = [i toNumber];
NSLog(@"var i is %@, number is %@",context[@"i"], number);
複製代碼
咱們能夠看到JSContext 調用 evaluateScript 方法 返回一個 JSValue 對象。經過value[i] 獲取到js 變量 i, 而後經過toNumber 方法將 js 變量類型轉換爲原生的變量類型,能夠經過點擊連接,來查看官網是怎麼實現js 值和原生值之間的轉換的。
咱們來列舉一下咱們比較經常使用的 2 個轉換類型的方法:
在OC代碼中使用 JavaScript 的函數, 咱們能夠經過callWithArguments 方法並傳入參數,並實現函數的調用,咱們能夠用如下代碼來幫助理解:
JSContext *context = [[JSContext alloc] init];
// 解析執行 JavaScript 腳本
[context evaluateScript:@"function addition(x, y) { return x + y}"];
// 得到 addition 函數
JSValue *addition = context[@"addition"];
// 以數組的形式傳入參數執行 addition 函數
JSValue *resultValue = [addition callWithArguments:@[@(4), @(8)]];
// 將 addition 函數執行的結果轉成原生 NSNumber 來使用。
NSLog(@"function is %@; reslutValue is %@",addition, [resultValue toNumber]);
複製代碼
從代碼中能夠看出,JSContext 先經過evaluateScript 方法獲取 JavaScript 代碼中的 JSValue類型的 addtion 函數, 再經過JSValue 的callWithArguments 方法,經過數組的形式傳入函數所需參數x、y來執行函數。
咱們一般使用invokeMethod:withArguments 方法來調用 JavaScript 中的全局函數。例如,Weex 框架 就是使用的這個方法來獲取JS中的全局函數的。
代碼的路徑是incubator-weex/ios/sdk/WeexSDK/Sources/Bridge/WXJSCoreBridge.mm ,核心代碼以下所示:
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args {
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}
複製代碼
從以上代碼中能夠看出,JSContext 先經過[_jsContext globalObject] 獲取到 JSValue 類型的屬性globalObject ,屬性中記錄了 JSContext 的全局對象,使用 globalObject 執行的 JavaScript 函數可以使用全局 JavaScript 對象。所以,經過 globalObject 執行 invokeMethod:withArguments 方法就可以去使用全局 JavaScript 對象了。
咱們先給出一段代碼來幫助你們理解 JavaScript 怎樣 調用原生代碼的:
// 在 JSContext 中使用原生 Block 設置一個減法 subtraction 函數
context[@"subtraction"] = ^(int x, int y) {
return x - y;
};
// 在同一個 JSContext 裏用 JavaScript 代碼來調用原生 subtraction 函數
JSValue *subValue = [context evaluateScript:@"subtraction(4,8);"];
NSLog(@"substraction(4,8) is %@",[subValue toNumber]);
複製代碼
從以上的代碼能夠看出,JavaScript 經過 Block 調用原生代碼的方式是:
在原生代碼中讓遵循 JSExport 協議的類,可以供 JavaScript 使用。在Weex 框架中,就有一個遵循了 JSExport 協議的 WXPolyfillSet 類,使得 JavaScript 也可以使用原生代碼中的 NSMutableSet 類型。
WXPolyfillSet 的頭文件代碼路徑是 incubator-weex/ios/sdk/WeexSDK/Sources/Bridge/WXPolyfillSet.h,內容以下:
@protocol WXPolyfillSetJSExports <JSExport>
// JavaScript 可使用的方法
+ (instancetype)create;
- (BOOL)has:(id)value;
- (NSUInteger)size;
- (void)add:(id)value;
- (BOOL)delete:(id)value;
- (void)clear;
@end
// WXPolyfillSet 遵循 JSExport 協議
@interface WXPolyfillSet : NSObject <WXPolyfillSetJSExports>
@end
複製代碼
咱們從上面的代碼中能夠看出WXPolyfillSet 經過 JSExport 協議,提供了一系列方法給 JavaScript 使用。
咱們知道 JavaScript 和原生之間的互通依賴於虛擬機環境 JSVirtualMachine。接下來就讓咱們深刻的瞭解 JavaScriptCore 引擎吧,瞭解了以後咱們會知道JavaScriptCore 是怎麼經過直接使用緩存JIT 編譯的機器碼來提升性能的,又是怎麼對部分函數進行性能測試編譯優化的。
JSVirtualMachine 是一個抽象的 JavaScript 虛擬機,是提供給開發者進行開發的,而其核心的 JavaScriptCore 引擎則是一個真實的虛擬機,包含了虛擬機都有的解釋器和運行時部分,其中,解釋器主要是來將高級的腳本語言編譯成字節碼,運行時主要用來管理運行時的內存空間。當內存出現問題的時候,須要調試內存問題時候,咱們科室使用JavaScriptCCore 裏面的 WebInspector,或者經過手動觸發 Full GC的方式來排查內存的問題。
JavaScriptCore 內部是由 Parser、Interpreter、Compiler、GC等部分組成,其中Compiler 負責把字節碼翻譯成爲機器碼,並進行優化。咱們能夠查看 WebKit 官方文檔來查看JavaScriptCore 引擎的介紹。
JavaScriptCore 解釋執行 JavaScript 代碼的流程,能夠分爲如下兩步。
若是你們想更深刻的瞭解JavaScript 引擎,這裏有一篇戴銘大神的博客,能夠幫助你更好地瞭解,點擊連接查看。
目前Objective-C 使用的是ARC,不能自動解決循環引用的問題,須要咱們程序員手動去解除循環,可是 JavaScript 使用的是GC(垃圾回收機制),全部的引用都是強引用,同時垃圾回收器能夠幫咱們解決循環引用的問題, JavaScriptCore 也是同樣的,通常來講,大多數狀況下不須要咱們去手動的管理內存。
有兩個狀況須要咱們注意一下:
若是增長的話,只可以在JavaScript 中爲這個Objective-C 對象增長一個額外的成員變量,可是在原生代碼中並不會同步增長這個成員變量,這樣作沒意義而且還可能形成一些奇怪的內存問題。
不要直接將一個 JSValue 類型的對象當成屬性或者成員變量保存在一個Objective-C對象中,特別是當這個Objective-C對象仍是暴露給JavaScript的時候,這樣作的話會致使循環引用。以下圖所示:
Objective-C不能直接強引用 JSValue類型的對象,其實也是不能直接弱引用的,若是弱引用的話,JSValue 對象就會被釋放了。以下圖所示:舉個例子說明一下:
//定義一個JSExport protocol
@protocol JSExportTest <JSExport>
//用來保存JS的對象
@property (nonatomic, strong) JSvalue *jsValue;
@end
//建一個對象去實現這個協議:
@interface JSProtocolObj : NSObject<JSExportTest>
@end
@implementation JSProtocolObj
@synthesize jsValue = _jsValue;
@end
//在VC中進行測試
@interface ViewController () <JSExportTest>
@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//建立context
self.context = [[JSContext alloc] init];
//設置異常處理
self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
[JSContext currentContext].exception = exception;
NSLog(@"exception:%@",exception);
};
//加載JS代碼到context中
[self.context evaluateScript:
@"function callback (){};
function setObj(obj) {
this.obj = obj;
obj.jsValue=callback;
}"];
//調用JS方法
[self.context[@"setObj"] callWithArguments:@[self.obj]];
}
複製代碼
上面的例子很簡單,調用JS方法,進行賦值,JS對象保留了傳進來的obj,最後,JS將本身的回調callback賦值給了obj,方便obj下次回調給JS;因爲JS那邊保存了obj,並且obj這邊也保留了JS的回調。這樣就造成了循環引用。
難道就沒有辦法了嗎?辦法是有的,只須要經過弱引用而且能保持JSValue對象不會被釋放就行。
在此,蘋果給出了一種新的引用關係,叫作 conditional ration ,就是有條件的強引用。經過這種引用咱們就能實現咱們想要的效果了。JSManageValue 就是蘋果用來實現 conditional ration 的一個類。
//從bundle中加載這段JS代碼。
NSString *multiplyScript = [self loadJSFromBundle];
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:multiplyScript];
JSValue *function = context[@"multiply"];
JSValue *result = [function callWithArguments:@[@5]];
JSManagedValue *managedValue = [JSManagedValue managedValueWithValue:result];
[context.virtualMachine addManagedReference:managedValue withOwner:self];
複製代碼
如下是JSManagedValue的通常使用步驟:
第一步,用JSValue對象建立一個JSManagedValue對象,JSManagedValue裏面其實就是包着一個JSValue對象,能夠經過它裏面一個只讀的value屬性取到,這一步實際上是添加一個對JSValue的弱引用。若是是隻執行這一步的話,JSValue會在其對應的JS值被垃圾回收器回收以後被釋放。所以咱們還須要執行第二步。
第二步,在虛擬機上爲這個JSManagedValue對象添加Owner(這個虛擬機就是給JS執行提供資源的,待會再講),這樣作以後,就給JSValue增長一個強關係,只要如下兩點有一點成立,這個JSManagedValue裏面包含的JSValue就不會被釋放:
一、JSValue對應的JS值沒有被垃圾回收器回收。
二、Owner對象沒有被釋放。
這樣作,就即避免了循環引用,又保證了JSValue不會由於弱引用而被馬上釋放。
咱們先來講一下 JSVirtualMachine,它爲JavaScript 的運行提供了底層資源,有本身獨立的堆棧以及垃圾回收機制。
JSVirtualMachine 仍是JSContext 的容器,能夠包含若干個JSContext,在一個進程之中,能夠存在多個JSVirtualMachine,JSVirtualMachine/JSContext/JSValue之間的關係咱們前面 1.2 章節說過,咱們能夠在同一個 JSVirtualMachine 的不一樣 JSContext 中互相傳遞 JSValue ,可是咱們不能在不一樣的 JSVirtualMachine 中的 JSContext 之間傳遞 JSValue。
這些都是由於每個 JSVirtualMachine 都有本身獨立的堆棧和垃圾回收器,一個 JSVirtualMachine 的垃圾回收器不知道怎麼處理從另外一個堆棧傳遞過來的值。
事實上,JavaScriptCore 提供的API 自己就是線程安全的。
咱們能夠在不一樣的線程之中建立 JSValue,使用 JSContext 執行JS語句,可是當一個線程正在執行 JS語句的時候,其餘線程想要使用這個正在執行 JS 語句的 JSContext 所屬的 JSVirtualMachine 就必須得等待,等待前前一個線程執行完,才能使用這個JSVirtualMachine。
這個強制串行的粒度是 JSVirtualMachine,若是你想要在不用線程中併發執行JS代碼,能夠爲不一樣的線程建立不一樣 JSVirtualMachine。
在 UIWebView 的代理方法中獲取 JSContext,代碼以下:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
}
複製代碼
上面代碼中咱們使用了私有屬性 "documentView.webView.mainFrame.javaScriptContext" ,可能會被蘋果拒絕上架。這裏咱們要注意的是每一個頁面加載完都是一個新的context,可是都是同一個JSVirtualMachine。若是 JavaScript 調用OC方法進行操做UI的時候,請注意當前線程是否是在主線程。
本章文章我跟你們分享了 JavaScriptCore 框架的組成、JavaScript 和原生交互、JavaScriptCore 引擎、內存管理、多線程、獲取 UIWebView 中的 JSContext等內容,或許我寫的也不是太完整,但願你們能留言溝通指出問題,並進一步探討關於 JavaScriptCore 相關的內容。
參考