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