本文由我們團隊的王瑞華童鞋撰寫。
OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 庫,它把 WebKit 的 JavaScript 引擎用 Objective-C 封裝,提供了簡單,快速以及安全的方式接入世界上最流行的語言。在專案的實際開發中,由於需要與 H5 端有互動,所以採用了JavaScriptCore的互動方式,藉此參與該專案的機會,對JavaScriptCore也有了一些簡單的瞭解。
JSContext/JSValue
JSContext 是 JavaScript 的執行環境。所有 JavaScript 執行發生的背景,以及所有 JavaScript 值被綁在這一個上下文中。JSContext 類似於 UIWindow 的概念,所有的執行都在改環境中發生:
1 2 3 4 5 6 7 |
JSContext *context = [[JSContext alloc] init]; [context evaluateScript:@"var num = 5 + 5"]; [context evaluateScript:@"var names = ['Grace', 'Ada', 'Margaret']"]; [context evaluateScript:@"var triple = function(value) { return value * 3 }"]; JSValue *tripleNum = [context evaluateScript:@"triple(num)"]; NSLog(@"Tripled: %d", [tripleNum toInt32]); // Tripled: 30 |
程式碼最後一行,每個 JSValue 起源於 JSContext 和 持有它的強引用。當一個 JSValue 例項方法創造一個新的 JSValue時,該新 JSValue 起源於同一個 JSContext。 JSValue 包裝了每一個可能的 JavaScript 值:字串和數字;陣列、物件和方法;甚至錯誤和特殊的 JavaScript 值諸如 null 和 undefined。
OBJECTIVE-C TYPE | JAVASCRIPT TYPE |
---|---|
nil | undefined |
NSNull | null |
NSString | string |
NSNumber | number, boolean |
NSDictionary | Object object |
NSArray | Array object |
NSDate | Date object |
NSBlock (1) | Function object (1) |
id (2) | Wrapper object (2) |
Class (3) | Constructor object (3) |
下標支援
作為下標傳遞的物件鍵將被轉換為一個 JavaScript 值, 然後值轉換為一個用作獲取屬性名稱的字串。
1 2 3 4 |
JSValue *names = context[@"names"]; JSValue *initialName = names[0]; NSLog(@"The first name: %@", [initialName toString]); // The first name: Grace |
該簡易方法對應於以下兩個方法:
1 2 |
- (JSValue *)objectForKeyedSubscript:(id)key; - (JSValue *)objectAtIndexedSubscript:(NSUInteger)index; |
呼叫方法
JSValue 包裝了一個 JavaScript 函式,我們可以從 Objective-C 程式碼中使用 Foundation 型別作為引數來直接呼叫該函式。
1 2 3 |
JSValue *tripleFunction = context[@"triple"]; JSValue *result = [tripleFunction callWithArguments:@[@5] ]; NSLog(@"Five tripled: %d", [result toInt32]); |
JavaScript 呼叫
上面說明的 OC 呼叫 JavaScript 的方法。那麼接下來,說明 JavaScript 訪問 OC 定義的物件和方法。我粗淺的認為就是在遵守該協議後,JavaScript 就可以呼叫 OC 實現的方法和屬性等。
讓 JSContext 訪問我們的本地客戶端程式碼的方式主要有兩種:JSExport 協議和block。
JSExport 協議
JSExport 提供一個將 OC 中的類、例項方法和屬性等匯出為 JavaScript 函式的方法。
該協議只包含了一個方法:
1 |
JSExportAs(PropertyName, Selector) |
即當匯出到 JavaScript 時,重新命名一個 selector。
這就需要熟悉它的做法,當帶有一個或多個引數的 seletor 轉變為 JavaScript 的屬性名字時,在預設情況下一個屬性名字將採用以下方式生成:
- 所有的冒號將從 Selector 當中移除掉
- 任何一個後邊跟著冒號的小寫字母開頭的單詞將被改換為大寫。
在這種預設的轉換方式下,一個 selector 如 doFoo:withBar: 將被導為doFooWithBar 。這種預設的改變有可能被 JSExportAs 巨集 所改寫,例如將 doFoo:withBar: 匯出為 doFoo 。實際寫法如下:
1 2 3 4 5 |
@protocol MyClassJavaScriptMethods JSExportAs(doFoo, - (void)doFoo:(id)foo withBar:(id)bar ); @end |
請注意 JSExport 巨集只可能適用於帶有一個或更多的引數的selector。
Blocks
當一個 Objective-C block 被賦給 JSContext 裡的一個識別符號,JavaScriptCore 會自動的把 block 封裝在 JavaScript 函式裡。如下:
1 2 3 4 5 6 7 8 |
JSContext *context = [[JSContext alloc] init]; context[@"simplifyString"] = ^(NSString *input) { NSMutableString *mutableString = [input mutableCopy]; CFStringTransform((__bridge CFMutableStringRef)mutableString, NULL, kCFStringTransformToLatin, NO); return mutableString; }; NSLog(@"%@", [context evaluateScript:@"simplifyString('什麼!')"]); // shén me! |
JSManagedValue 與記憶體管理
由於 block 可以保有變數引用,而且 JSContext 也強引用它所有的變數,為了避免強引用迴圈需要特別小心。OC 採用的是引用計數機制,而 JavaScript採用的垃圾回收機制。基於此項機制,便出現了 JSManagedValue ,它的主要用途是儲存一個 JSValue,這樣可以避免一個保留環的產生。
JavaScript 與 OC 互動的實際例項
在開發專案中,我們採用了 JavaScript 的方式完成了與 Native 的互動,使得比單純的運用 UIWebview 的 stringByEvaluatingJavaScriptFromString: 更加高效。
如上所述,讓 JSContext 訪問我們的本地客戶端程式碼的方式主要有兩種:JSExport 協議和block。在我們專案中主要採用了 JSExport 的方式去實現。
1. JDRWebViewJSExportProtocol 和 JDRWebViewBaseHandler
我們的 JDRWebViewBaseHandler 類實現了 JDRWebViewJSExportProtocol 協議,該協議規定了那些方法或者屬性可在 JavaScript 中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
@protocol JDRWebViewJSExportProtocol JSExportAs (closeForJS /**H5呼叫的 Webview 關閉方法的別名**/, @optional - (void)close:(id)JSON callback:(JSValue*) callback ); JSExportAs (goBackForJS /**H5呼叫的 Webview 關閉方法的別名**/, @optional - (void)goBack:(id)JSON callback:(JSValue*) callback ); @end @interface JDRWebViewBaseHandler : NSObject @property (nonatomic , weak) JDRWebViewController *webViewController; -(instancetype)initWithWebViewController:(JDRWebViewController *) webViewController NS_DESIGNATED_INITIALIZER; @end @implementation JDRWebViewBaseHandler -(instancetype)initWithWebViewController:(JDRWebViewController *) webViewController{ if (self = [super init]) { _webViewController = webViewController; } return self; } -(void)goBack:(id)JSON callback:(JSValue *)callback { WEAK_SELF(weakSelf); dispatch_async(dispatch_get_main_queue(), ^{ STRONG_SELF(strongSelf, weakSelf); //判斷 backPressCallBack 是否已經設定 ,如果已經設定監聽方法 , 則執行 監聽回退的 JS 方法 if ([strongSelf.webViewController.webView canGoBack]) { [(UIWebView*)strongSelf.webViewController.webView stopLoading]; [(UIWebView*)strongSelf.webViewController.webView goBack]; }else { [strongSelf close:nil callback:nil]; } }); } -(void)close:(id)JSON callback:(JSValue *)callback { //目前關閉 WebView 方法不關注 callback 引數 WEAK_SELF(weakSelf); dispatch_async(dispatch_get_main_queue(), ^{ STRONG_SELF(strongSelf, weakSelf); if ([strongSelf.webViewController.navigationController popViewControllerAnimated:YES] == nil) { [strongSelf.webViewController dismissViewControllerAnimated:YES completion:nil]; } }); } @end |
2. JSContext 配置
然後,我們可以用我們已經建立的 JDRWebViewBaseHandler 類,我們需要將其匯出到 JavaScript 環境中。
1 2 3 |
self.JSHandler = [[JDRWebViewBaseHandler alloc] initWithWebViewController:self]; // 以 JSExport 協議關聯 native 的方法 *platform* 是暫時定義的關鍵字 self.context[@"platform"] = self.JSHandler; |
3.在 H5 頁面中書寫 JavaScript 函式呼叫 OC
1 2 3 |
close: function() { platform.coselForJS(); } |
結語
通常對於 JavaScript 與 OC 的互動,會採用 JavaScriptCore 包裝一層協議方法,原始一點的方法則是通過擷取 UIWebView 的協議實現。隨著 iOS 8 之後 WKWebview的出現,又出現了一個新的方式,而對於 WKWebView 來說是不支援直接與 JavaScriptCore 互動的。通常採取 WKUserContentController 採用它的協議方法
1 |
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
如果使用 WKWebview 的話,對於 H5 中的 JavaScript 寫法又需要改動為 window.webkit.messageHandlers..postMessage()。WKWebView 對於JavaScript 的呼叫只提供了一種方法:
1 |
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler |
目前專案中考慮到 應用 WKWebview 與 UIWebview + JavaScriptCore 呼叫方式的差異,暫時放棄了對於 WKWebview的支援。