
iOS 開發中,我們時不時的需要加載一些 Web 頁面,一些需求使用 Web 頁面來實現可以更可控,如上線後也可以發布更新,修改 UI 布局,或者修復 bug,這些 Web 頁面的作用不止是展示,很大一部分是需要和原生代碼實現的 UI 和業務邏輯發生交互的,那麼不可避免的,就需要用一些方法來實現 Web 頁面(主要是 JavaScript)和原生代碼之間的通信,在 JavaScriptCore 出現之前,很多項目都在用 WebViewJavascriptBridge 作為 Web 頁面和原生代碼之間的一個橋梁(bridge),來傳輸一些數據和方法的調用,如 Facebook Messenger,Facebook Paper 等。
WebViewJavascriptBridge 的原理是通過自定義 scheme,在加載一個特定標識的URL( wvjbscheme://__BRIDGE_LOADED__)時在 UIWebView 的代理方法 webView:shouldStartLoadWithRequest:navigationType: 中攔截 URL 並通過 UIWebView 的 stringByEvaluatingJavaScriptFromString: 方法執行一段 JS,這個 JS 文件中聲明了一些變量和方法,在通訊中作為一個橋梁,那麼怎麼通訊呢?
在 OC 中,實例化一個 WebViewJavascriptBridge 並調用 registerHandler:handler: 注冊並監聽一下事件,第一個參數是一個字符串,用來標識一個特定的事件,handler 是一個 block,方法內部將標識作為 key,handler 作為值保存。
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}當 JS 中需要調用 OC 的方法時,組裝一個類似結構的數據,一個字符串作為標識,將需要傳輸的數據作為值並保存在一個全局數組中
var sendMessageQueue = [];
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
// 主要就是這一行,將 message 保存到全局數組,供待會兒查詢
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}並觸發一個特定的 URL(wvjbscheme://__WVJB_QUEUE_MESSAGE__),UIWebView 則在 webView:shouldStartLoadWithRequest:navigationType: 中攔截這個 URL,並執行一段 JS(WebViewJavascriptBridge._fetchQueue();)
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}查詢 JS 中全局數組中的值,並轉成 JSON 字符串返回,OC 中拿到 JSON 字符串,並解析,得到一個數組,遍歷數組,根據數組中每個對象的 handlerName 查詢 OC 中是否有注冊這個事件,如果有注冊,則根據 handlerName 取出保存在字典中的 block,並執行這個 block,block 可以接收一個 id 類型的參數,將 JS 全局數組中根據 handlerName 取出來的數據作為參數傳入 block。這樣就實現了從 JS 到 OC 中的數據傳輸。
OC 中調用 JS 的方法相對簡單,因為 UIWebView 可以主動執行 JS,JS 中可以將需要監聽的事件注冊,同樣是字符串作為標識,一個函數作為值,保存到一個全局對象中,在 OC 中主動執行特定的 JS 方法時,將數據封裝成 JSON 字符串,傳入標識符和數據,並遍歷 JS 中保存 handler 的全局對象,看有沒有注冊相應的事件,如果有,根據 事件的名字得到一個函數並執行。實現了 OC 調用 JS 中的方法並向 JS 中傳輸數據。
iOS 7 開始,蘋果提供了一個叫作 JavaScriptCore 的框架,使用 JavaScriptCore 框架可以實現 OC 和 JS 的互相調用,而不需要依賴「橋」來實現,怎麼通訊呢?
在 JS 中定義一個方法
function alertFunc() {
window.alert("這是一個JS中的彈框!")
}在 webViewDidFinishLoad: 代理方法中,獲取到 JSContext 對象
- (void)webViewDidFinishLoad:(UIWebView *)webView {
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[context setExceptionHandler:^(JSContext *ctx, JSValue *expectValue) {
NSLog(@"%@", expectValue);
}];
self.context = context;
}在一個 button 的點擊事件中可以根據 JS 定義的方法的名字獲得一個 JSValue 類型對象,這個對象就是在 JS 中定義的方法,JSValue 對象通過調用 callWithArguments: 方法,執行這個 JS 方法。
- (IBAction)buttonClick:(UIButton *)sender {
if (!self.context) {
return;
}
JSValue *funcValue = self.context[@"alertFunc"];
[funcValue callWithArguments:nil];
}點擊按鈕時,效果如下。

實現了 OC 中調用 JS 的方法。
在 OC 中,通過給 JSContext 的一個 key 賦值,值為一個 block,key 是 JS 中調用的方法的名字,代碼如下:
self.context[@"ocAlert"] = ^{
// block 異步執行,如果涉及到 UI 的操作需要回到主線程操作
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:@"這是OC中的彈框!" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[alert dismissViewControllerAnimated:YES completion:^{
}];
}]];
[strongSelf.navigationController presentViewController:alert animated:YES completion:nil];
});
};在 Web 頁面中創建一個 button 並設置 button 的 onClick 事件調用 ocAlert 方法
點擊這裡
點擊 Web 頁面上的 button 按鈕,效果如下

實現了 JS 調用 OC 中的方法。
是不是方便了很多?
嗯 ,一篇文章應該有個寫在後面的。
以上當然只是 JavaScriptCore 框架的一個很小的應用,使用 JavaSciptCore 框架結合 Objective-C 的動態性可以做很多事,比如著名的熱修復框架 JSPatch 就是這兩者的結合。這裡只是演示了 JS 和 OC 之間的方法調用,並沒有傳輸數據,JavaScriptCore 框架是很容易的實現兩者之間的數據傳輸的。具體做法可以參考參考資料。
蘋果添加的這些新特性可以給開發帶來很多便利,就是不知道有坑沒有,嗯,且爬且珍惜吧。
使用 JavaScriptCore 進制通訊的 demo 放到了 GitHub,地址如下:
https://github.com/cielpy/CPYJSCoreDemo
JavaScriptCore by Example
JavaScriptCore初探
WebViewJavascriptBridge