大家好,今天筆者分享一下自己在項(xiàng)目開發(fā)中,對iOS和H5之間的通信的一些知識點(diǎn)。主要是針對于蘋果APP混合開發(fā)中的一點(diǎn)點(diǎn)小知識。然后是對WKWebViewJavascriptBridge,這個(gè)iOS原生OC代碼和H5之間的通信橋接第三方包的一些分析。
首先在這里,先說明,這篇文章iOS這塊的控件用的是WKWebview。因?yàn)樵賗OS12之后,UIWebview因?yàn)樾阅懿蛔愫蛢?nèi)存占有過大等一系列問題,Apple公司最終決定將它慢慢移出iOS的舞臺。
先來大致看下WKWebview一些常規(guī)用到的代理方法:
// --------------- WKNavigationDelegate -----------------
// 請求之間,決定是否跳轉(zhuǎn)網(wǎng)頁:一般用戶在點(diǎn)擊一個(gè)鏈接時(shí)跳轉(zhuǎn)到下一個(gè)頁面之間會調(diào)用這個(gè)方法
// 重點(diǎn)的代理方法,攔截webView 中url的方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
// 獲取響應(yīng)數(shù)據(jù)之后,決定是否跳轉(zhuǎn)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 開始加載頁面時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
// 主機(jī)地址被重定向時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
// 頁面加載失敗時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
// 頁面加載完畢時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
// 跳轉(zhuǎn)失敗時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
// 證書驗(yàn)證步驟:如果需要證書驗(yàn)證,與使用AFN進(jìn)行HTTPS證書驗(yàn)證是一樣的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;
// ------- WKUIDelegate方面一個(gè)比較重要的代理 ---------------
// 在webview中使用alert函數(shù)時(shí),需要執(zhí)行這個(gè)代理,否則將無法打開
// 壞處在于,需要我們硬性執(zhí)行代理方法,好處在于,可以在我們原生iOS這邊自定義彈出框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
接下來筆者說一下,最基礎(chǔ)的iOS和js之間的通信方式,在WKWebview這邊主要的方法:
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
這里我們和UIWebview中執(zhí)行通信代碼的方法比較一下:
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
可以看到,WKWebview這邊的通信方法相對而言比較的人性化,因?yàn)閕OS傳遞給js代碼之后,得到的結(jié)果不一定是NSString類型,在很多情況下我們會需要得到一個(gè)對象、數(shù)組等等數(shù)據(jù)。
接下來,順便提一下一個(gè)iOS和H5之間通信的架包:JavaScriptCore.framework。這個(gè)架包是蘋果自帶支持的架包,主要目的就是讓開發(fā)人員更加簡便的去操作iOS和H5之間的通信代碼。導(dǎo)入方式是直接從build phases中導(dǎo)入。如下圖:

接下來,我們正式進(jìn)入,對WKWebViewJavascriptBridge這個(gè)第三方包的使用和源碼的分析。首先,筆者這邊給出了一個(gè)簡單的使用WKWebViewJavascriptBridge的demo,代碼地址:https://github.com/IBIgLiang/WKWebView-WebViewJavascriptBridge
。而WKWebViewJavascriptBridge這個(gè)第三方包的GitHub地址如下:https://github.com/marcuswestin/WebViewJavascriptBridge。里面已經(jīng)很明確的指出的使用流程和注意事項(xiàng)。
這里筆者用一張圖來總結(jié)一下這個(gè)過程,當(dāng)然這些代碼在demo中都有,大家可以照著圖看下:

接下來,我們重點(diǎn)來看看這個(gè)第三方的源代碼,我們這里通過流程圖,大致看下整個(gè)過程。先說明,筆者這里暫時(shí)不考慮UIWebview的操作,因?yàn)楣P者已經(jīng)說了它將退出。作為iOS開發(fā),我們就從iOS這邊開始整個(gè)流程,先從ViewController開始:

從上圖中可以看出,一般來說,VC中所作的基本上都是準(zhǔn)備工作,無論是注冊通信還是響應(yīng)通信,除了當(dāng)響應(yīng)通信時(shí),沒有需要傳遞的參數(shù),因?yàn)閃ebViewJavascriptBridge本身所有的保存的參數(shù)的消息隊(duì)列是統(tǒng)一處理的。
接下來我們來看看,上面說過的一個(gè)重要的代理:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
這個(gè)代理主要是做請求攔截操作,WebViewJavascriptBridge作者在攔截的過程中,加載WebViewJavascriptBridge_JS文件,然后是處理消息隊(duì)列中的數(shù)據(jù),具體我們還是一起看流程圖:

這張圖的發(fā)起點(diǎn)是在H5的JS端:
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
// 創(chuàng)建一個(gè)看不見的iframe,發(fā)起一個(gè)請求,這個(gè)請求用來加載WebViewJavascriptBridge_JS.m中的js代碼
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
調(diào)用setupWebViewJavascriptBridge這個(gè)方法之后,通過iFrame發(fā)送https://bridge_loaded請求,進(jìn)入decidePolicyForNavigationAction代理中,然后走上面流程圖中的流程。當(dāng)?shù)刂肥莌ttps://bridge_loaded時(shí),此時(shí)直接進(jìn)入[_base injectJavascriptFile];代碼段。具體內(nèi)容如下:
//TODO:WebViewJavascriptBridge橋接js文件的導(dǎo)入,并且發(fā)送iOS端業(yè)務(wù)數(shù)據(jù)給js
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
//導(dǎo)入橋接文件之后,將iOS端需要發(fā)送的數(shù)據(jù)發(fā)送給js端
//self.startupMessageQueue中放置了iOS發(fā)起給js的數(shù)據(jù)
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
// 發(fā)送iOS端數(shù)據(jù)到JS端
[self _dispatchMessage:queuedMessage];
}
}
}
主要做的就是導(dǎo)入js橋接問題,然后就是發(fā)送iOS端的業(yè)務(wù)參數(shù),這個(gè)參數(shù)的來源已經(jīng)在上面ViewController準(zhǔn)備工作的流程圖中寫明。
除了JS端的iFrame發(fā)送https://bridge_loaded請求之外,還有WebViewJavascriptBridge_js中的https://wvjb_queue_message,這個(gè)請求發(fā)起的主要作用就是處理消息隊(duì)列中的信息。也就是[self WKFlushMessageQueue];這個(gè)步驟。這個(gè)消息隊(duì)列的處理包含了JS端發(fā)送的數(shù)據(jù)。代碼如下:
//TODO:處理消息隊(duì)列中的數(shù)據(jù)
- (void)WKFlushMessageQueue {
// 讀取js發(fā)送的數(shù)據(jù)
[_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
// 處理消息隊(duì)列中的數(shù)據(jù)
[_base flushMessageQueue:result];
}];
}
[_base flushMessageQueue:result];這個(gè)方法中包含了JS端發(fā)送的數(shù)據(jù)和iOS處理完數(shù)據(jù)之后回調(diào)給JS端的數(shù)據(jù)的處理,包括兩個(gè)部分:一個(gè)是iOS響應(yīng)JS端通信后JS端回調(diào)給iOS端的消息內(nèi)容,key值包含callbackId;另一個(gè)是JS響應(yīng)iOS端的通信的消息內(nèi)容,key值包含responseId。主要代碼段:
NSString* responseId = message[@"responseId"];
if (responseId) {
// 如果是js注冊函數(shù)中回調(diào)回來的數(shù)據(jù),就直接回到iOS的callHandler的block中
// js注冊registerHandler -> iOS發(fā)送相應(yīng)業(yè)務(wù)數(shù)據(jù)callHandler -> js中注冊函數(shù)的block回調(diào)到iOS中
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
// iOS注冊函數(shù), js相應(yīng)數(shù)據(jù)后,回到iOS注冊函數(shù)registerHandler中的block,然后回調(diào)給js
//iOS注冊registerHandler -> js發(fā)送相應(yīng)業(yè)務(wù)數(shù)據(jù)callHandler -> 進(jìn)入iOS的registerHandler的block中 -> responseCallback回調(diào)給js
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
// 將js發(fā)送給iOS之后,iOS處理完業(yè)務(wù),重新將業(yè)務(wù)數(shù)據(jù)發(fā)送給js端
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
到此,除了橋接js文件沒有涉及講解之外,其余重要步驟都已講解完畢。
============================================================
接下來就是最后的WebViewJavascriptBridge_JS文件。這個(gè)js文件的內(nèi)容其實(shí)很好理解,就是為iOS和JS兩端的通信創(chuàng)建一個(gè)通用的WebViewJavascriptBridge對象:
// 定義一個(gè)webview和js之間的橋
window.WebViewJavascriptBridge = {
// 用于JS端注冊通信
registerHandler: registerHandler,
// 用于存儲JS端相應(yīng)iOS端的通信的參數(shù),放入_fetchQueue中
callHandler: callHandler,
// 請求超時(shí)字段
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
// 消息隊(duì)列,用于放置JS端發(fā)送給iOS端的數(shù)據(jù)
_fetchQueue: _fetchQueue,
// 處理iOS端發(fā)送給JS端的數(shù)據(jù)
_handleMessageFromObjC: _handleMessageFromObjC
};
這個(gè)文件里面的方法中,需要重點(diǎn)說明一下的就是_dispatchMessageFromObjC這個(gè)方法中的這個(gè)代碼段:
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
其實(shí)大家可以發(fā)現(xiàn),這個(gè)代碼段和筆者上面貼出來flushMessageQueue部分的那個(gè)代碼段是相對應(yīng)的,它就是JS端處理iOS發(fā)送過來的數(shù)據(jù)的過程。這里筆者就不具體展開了。
由于篇幅長度原因,關(guān)于iOS中的WKWebView和H5之間通信及WKWebViewJavascriptBridge的源碼分析就到此為止了。
筆者將在下一篇文章WKURLSchemeHandler在WKWebView的攔截請求中的使用中指出NSURLProtocol攔截這WKWebView無效的問題以及原因(scheme底層注冊有關(guān)),并且使用WKURLSchemeHandler攔截請求,當(dāng)前有個(gè)前提是這個(gè)請求的scheme是自定義的,有興趣的同學(xué)可以瞄一眼!