iOS開發(fā)WebViewJavascriptBridge通訊原理解析
前言
H5頁面具有跨平臺、開發(fā)容易、上線不需要跟隨App的版本等優(yōu)點,但H5頁面也有體驗不如native好、沒有native穩(wěn)定等問題。所以目前大部分App都是使用Hybrid混合開發(fā)的。
當然有了H5頁面就少不了H5與native交互,交互就會用到bridge的能力了。
WebViewJavascriptBridge是一個native與JS進行消息互通的第三方庫,本章會簡單解析一下WebViewJavascriptBridge的源碼和實現(xiàn)原理。
通訊原理
JavaScriptCore
JavaScriptCore作為iOS的JS引擎為原生編程語言OC、Swift 提供調(diào)用 JS 程序的動態(tài)能力,還能為 JS 提供原生能力來彌補前端所缺能力。 iOS中與JS通訊使用的是JavaScriptCore庫,正是因為JavaScriptCore這種起到的橋梁作用,所以也出現(xiàn)了很多使用JavaScriptCore開發(fā)App的框架,比如RN、Weex、小程序、Webview Hybrid等框架。 如圖:

當然JS引擎不光有蘋果的JavaScriptCore,谷歌有V8引擎、Mozilla有SpiderMoney
JavaScriptCore本章只簡單介紹,后面主要解析WebViewJavascriptBridge。因為uiwebview已經(jīng)不再使用了,所以后面提到的webview都是wkwebview,demo也是以wkwebview進行解析。
源碼解析
代碼結(jié)構(gòu)
除了引擎層外,還需要native、h5和WebViewJavascriptBridge三層才能完成一整個信息通路。WebViewJavascriptBridge就是中間那個負責通信的SDK。
WebViewJavascriptBridge的核心類主要包含幾個:
- WebViewJavascriptBridge_JS:是一個JS的字符串,作用是JS環(huán)境的Bridge初始化和處理。負責接收native發(fā)給JS的消息,并且把JS環(huán)境的消息發(fā)送給native。
- WKWebViewJavascriptBridge/WebViewJavascriptBridge:主要負責WKWebView和UIWebView相關環(huán)境的處理,并且把native環(huán)境的消息發(fā)送給JS環(huán)境。
- WebViewJavascriptBridgeBase:主要實現(xiàn)了native環(huán)境的Bridge初始化和處理。

初始化
WebViewJavascriptBridge是如何完成初始化的呢,首先要有webview容器,所以要對webview容器進行初始化,設置代理,初始化WebViewJavascriptBridge對象,加載URL。
WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds];
webView.navigationDelegate = self;
[self.view addSubview:webView];
// 開啟打印
[WebViewJavascriptBridge enableLogging];
// 創(chuàng)建bridge對象
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
// 設置代理
[_bridge setWebViewDelegate:self];
這里加載的就是JSBridgeDemoApp這個本地的html文件。
NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"JSBridgeDemoApp" ofType:@"html"];
NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
[webView loadHTMLString:appHtml baseURL:baseURL];
再看一下JSBridgeDemoApp這個html文件。
function setupWebViewJavascriptBridge(callback) {
// 第一次調(diào)用這個方法的時候,為false
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
// 第一次調(diào)用的時候,為false
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
// 把callback對象賦值給對象
window.WVJBCallbacks = [callback];
// 加載WebViewJavascriptBridge_JS中的代碼
// 相當于實現(xiàn)了一個到https://__bridge_loaded__的跳轉(zhuǎn)
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)
}
// 驅(qū)動所有hander的初始化
setupWebViewJavascriptBridge(function(bridge) {
...
}
在JSBridgeDemoApp的script標簽下,聲明了一個名為setupWebViewJavascriptBridge的方法,在加載html后直接進行了調(diào)用。 setupWebViewJavascriptBridge方法中最核心的代碼是:

創(chuàng)建一個iframe標簽,然后加載了鏈接為 https://bridge_loaded 的內(nèi)容。相當于在當前頁面內(nèi)容實現(xiàn)了一個到 https://bridge_loaded 的內(nèi)部跳轉(zhuǎn)。 ps:iframe標簽用于在網(wǎng)頁內(nèi)顯示網(wǎng)頁,也使用iframe作為鏈接的目標。
html文件內(nèi)部實現(xiàn)了這個跳轉(zhuǎn)后native端是如何監(jiān)聽的呢,在webview的代理里有一個方法:decidePolicyForNavigationAction 這個代理方法的作用是只要有webview跳轉(zhuǎn),就會調(diào)用到這個方法。代碼如下:
// 只要webview有跳轉(zhuǎn),就會調(diào)用webview的這個代理方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
// 如果是WebViewJavascriptBridge發(fā)送或者接收消息,則特殊處理。否則按照正常流程處理
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
// 是否是 https://__bridge_loaded__ 這種初始化加載消息
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
// https://__wvjb_queue_message__
// 處理WEB發(fā)過來的消息
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
// webview的正常代理執(zhí)行流程
...
從上面的代碼中可以看到,如果監(jiān)聽的webview跳轉(zhuǎn)不是WebViewJavascriptBridge發(fā)送或者接收消息就正常執(zhí)行流程,如果是WebViewJavascriptBridge發(fā)送或者接收消息則對此攔截不跳轉(zhuǎn),并且針對消息進行處理。 當消息url是https://bridge_loaded 的時候,會去注入WebViewJavascriptBridge_js到JS中:
// 將WebViewJavascriptBrige_JS中的方法注入到webview中并且執(zhí)行
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
// 把javascript代碼注入webview中執(zhí)行
[self _evaluateJavascript:js];
// javascript環(huán)境初始化完成以后,如果有startupMessageQueue消息,則立即發(fā)送消息
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
[self _evaluateJavascript:js];就是執(zhí)行webview中的evaluateJavaScript:方法。把JS寫入webview。所以執(zhí)行完此處代碼JS當中就有bridge這個對象了。初始化完成。
總結(jié):在加載h5頁面后會調(diào)用setupWebViewJavascriptBridge方法,該方法內(nèi)創(chuàng)建了一個iframe加載內(nèi)容為 https://bridge_loaded ,該消息被decidePolicyForNavigationAction監(jiān)聽到,然后執(zhí)行injectJavascriptFile去讀取WebViewJavascriptBridge_js將WebViewJavascriptBridge對象注入到當前h5中。
WebViewJavascriptBridge 對象
整個WebViewJavascriptBridge_js文件其實就是一個字符串形式的js代碼,里面包含WebViewJavascriptBridge和相關bridge調(diào)用的方法。
// 初始化Bridge對象,OC可以通過WebViewJavascriptBridge來調(diào)用JS里面的各種方法
window.WebViewJavascriptBridge = {
registerHandler: registerHandler, // JS中注冊方法
callHandler: callHandler, // JS中調(diào)用OC的方法
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue, // 把消息轉(zhuǎn)換成JSON串
_handleMessageFromObjC: _handleMessageFromObjC // OC調(diào)用JS的入口方法
};
WebViewJavascriptBridge對象里核心的方法有:
- registerHandler:JS中注冊方法
- callHandler: JS中調(diào)用native的方法
- _fetchQueue: 把消息轉(zhuǎn)換成JSON字符串
- _handleMessageFromObjC:native調(diào)用JS的入口方法
當初始化完成后,WebViewJavascriptBridge對象和對象里的方法就已經(jīng)存在并且可用了。
JS和native是如何相互傳遞消息的呢?從上面的代碼中可以看到如果JS想要發(fā)送消息給native就會調(diào)用callHandler方法;如果native想要調(diào)用JS方法那JS側(cè)就必須先注冊一個registerHandler方法。
相對應的我們看一下native側(cè)是如何與JS傳遞消息的,其實接口標準是一致的,native調(diào)JS的方法使用callHandler方法:
id data = @{ @"dataFromOC": @"aaaa!" };
[_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) {
NSLog(@"JS回調(diào)的數(shù)據(jù)是:%@", response);
}];
JS調(diào)native方法在native側(cè)就必須先注冊一個registerHandler方法:
// 注冊事件(h5調(diào)App)
[_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"JSTOOCCallback called: %@", data);
responseCallback(@"Response from JSTOOCCallback");
}];
也就是說native像JS發(fā)送消息的話,JS側(cè)要先注冊該方法registerHandler,native側(cè)調(diào)用callHandler; JS像native發(fā)送消息的話,native側(cè)要先注冊registerHandler,JS側(cè)調(diào)用callHandler。這樣才能完成雙端通信。
如圖:

native向JS發(fā)送消息
現(xiàn)在要從native側(cè)向JS側(cè)發(fā)送一條消息,方法名為:"OCToJSHandler",并且拿到JS的回調(diào),具體實現(xiàn)細節(jié)如下:
JS側(cè)
native向JS發(fā)送數(shù)據(jù),首先要在JS側(cè)去注冊這個方法:
bridge.registerHandler('OCToJSHandler', function(data, responseCallback) {
...
})
這個registerHandler的實現(xiàn)在WebViewJavascriptBridge_JS是:
// web端注冊一個消息方法,將注冊的方法存儲起來
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
就是將這個注冊的方法存儲到messageHandlers這個map中,key為方法名稱,value為function(data, responseCallback) {}這個方法。
native側(cè)
native側(cè)調(diào)用bridge的callHandler方法,傳參為data和一個callback回調(diào)
id data = @{ @"dataFromOC": @"aaaa!" };
[_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) {
NSLog(@"JS回調(diào)的數(shù)據(jù)是:%@", response);
}];
接下來會走到WebViewJavascriptBridgeBase的-sendData: responseCallback: handlerName:方法,該方法中將"data"和"handlerName"存入到一個message字典中,如果存在callback會生成一個callbackId一并存入到message字典中,并且將該回調(diào)存入到responseCallbacks中,key為callbackId,value為這個callback。代碼如下:
// 所有信息存入字典
NSMutableDictionary* message = [NSMutableDictionary dictionary];
if (data) {
message[@"data"] = data;
}
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
將message存儲到隊列等待執(zhí)行,執(zhí)行該條message時會先將message進行序列化,序列化完成后將message拼接到字符串WebViewJavascriptBridge._handleMessageFromObjC('%@');中,然后執(zhí)行_evaluateJavascript執(zhí)行該js方法。
// 把OC消息序列化、并且轉(zhuǎn)化為JS環(huán)境的格式,然后在主線程中調(diào)用_evaluateJavascript
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
[self _evaluateJavascript:javascriptCommand];
}
_handleMessageFromObjC方法會將messageJSON傳遞給_dispatchMessageFromObjC進行處理。 首先將messageJSON進行解析,根據(jù)handlerName取出存儲在messageHandlers中的方法。如果該message中存在callbackId,將callbackId作為參數(shù)生成一個回調(diào)放到responseCallback中。 代碼如下:
function _doDispatchMessageFromObjC() {
// 解析發(fā)送過來的JSON
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 主動調(diào)用
// 如果有callbackid
if (message.callbackId) {
// 將callbackid當做callbackResponseId再返回回去
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
// 把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
// 獲取JS注冊的函數(shù),取出消息里的handlerName
var handler = messageHandlers[message.handlerName];
// 調(diào)用JS中的對應函數(shù)處理
handler(message.data, responseCallback);
}
}
handler方法其實就是名為"OCToJSHandler"的方法,這時就走到了registerHandler里的那個function(data, responseCallback) {}方法了。我們看一下方法內(nèi)部的具體實現(xiàn):
bridge.registerHandler('OCToJSHandler', function(data, responseCallback) {
// OC中傳過來的數(shù)據(jù)
log('從OC傳過來的數(shù)據(jù)是:', data)
// JS返回數(shù)據(jù)
var responseData = { 'dataFromJS':'bbbb!' }
responseCallback(responseData)
})
data就是從native傳過來的數(shù)據(jù),responseCallback就是保存的回調(diào),然后又生成了新數(shù)據(jù)作為參數(shù)給到了這個回調(diào)。
responseCallback的實現(xiàn)是:
responseCallback = function(responseData) {
// 把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
將該方法的handlerName、生成的callbackResponseId(也就是callbackId)以及JS返回的數(shù)據(jù)一起給到_doSend方法。
_doSend方法將message存儲到sendMessageQueue消息列表中,并使用messagingIframe加載了一次https://wvjb_queue_message。
// 把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作
function _doSend(message, responseCallback) {
// 把消息放入消息列表
sendMessageQueue.push(message);
// 發(fā)出js對oc的調(diào)用,讓webview執(zhí)行跳轉(zhuǎn)操作,可以在decidePolicyForNavigationAction:中攔截到js發(fā)給oc的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
這時webview的監(jiān)聽方法decidePolicyForNavigationAction監(jiān)聽到了https://wvjb_queue_message 消息后還是執(zhí)行WebViewJavascriptBridge._fetchQueue()去取數(shù)據(jù),取到數(shù)據(jù)后根據(jù)responseId當初在_responseCallbacks中存儲的callback,然后執(zhí)行callback、移除responseCallbacks中的數(shù)據(jù)。到此為止,整個native向JS發(fā)送消息的過程就完成了。
總結(jié):
- JS中先調(diào)用registerHandler將方法存儲到messageHandlers中
- native調(diào)用callHandler:方法,將消息內(nèi)容存儲到message中,回調(diào)存儲到responseCallbacks中。
- 將message消息序列化通過_evaluateJavascript方法執(zhí)行_handleMessageFromObjC
- 將message解析,通過message.handlerName從messageHandlers取出該方法;根據(jù)message.callbackId生成回調(diào)
- 執(zhí)行該方法,回調(diào)
JS向native發(fā)送消息
從JS向native發(fā)消息其實和native向JS發(fā)消息的接口層面是差不多的。
native側(cè)
native側(cè)首先要注冊一個JSTOOCCallback方法
[_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
responseCallback(@"Response from JSTOOCCallback");
}];
該方法也同樣是將該方法的callback存儲起來,存儲到messageHandlers當中,key就是方法名"JSTOOCCallback",value就是callback。
JS側(cè)
JS側(cè)會調(diào)用callHandler方法:
// 調(diào)用oc中注冊的那個方法
bridge.callHandler('JSTOOCCallback', {'foo': 'bar'}, function(response) {
log('JS 取到的回調(diào)是:', response)
})
這個callHandler方法同樣會調(diào)用_doSend方法:將callback存儲到responseCallbacks中,key為callbakid;將消息存儲到sendMessageQueue中;messagingIframe執(zhí)行https://wvjb_queue_message
native的decidePolicyForNavigationAction方法監(jiān)聽到該消息后同樣通過WebViewJavascriptBridge._fetchQueue()去取消息。
根據(jù)callbackId創(chuàng)建一個responseCallback,根據(jù)message的handlerName從messageHandlers取出該回調(diào),然后執(zhí)行:
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
handler(message[@"data"], responseCallback);
調(diào)用完這個方法后,該消息已經(jīng)收到,然后將回調(diào)的內(nèi)容回調(diào)給JS。 通過上面的代碼可以看到,回調(diào)JS的內(nèi)容就是callbackId和responseData生成的message,調(diào)用_queueMessage方法。
_queueMessage方法上面已經(jīng)看過了,就是序列化消息、加入隊列、執(zhí)行WebViewJavascriptBridge._handleMessageFromObjC('%@');方法。
JS收到該消息后,處理返回的消息,從responseCallbacks中根據(jù)message中的responseId取出callback并且執(zhí)行。最后刪除responseCallbacks中的數(shù)據(jù),JS向native發(fā)送數(shù)據(jù)就完成了。
小結(jié):
- native側(cè)調(diào)用registerHandler方法注冊方法,方法名為JSTOOCCallback,將消息存儲到messageHandlers中,key為方法名,value為callback。
- JS側(cè)調(diào)用callHandler方法:將responseCallback存儲到responseCallbacks中;將message存儲到sendMessageQueue中;messagingIframe執(zhí)行 http://wvjb_queue_message
- native側(cè)監(jiān)聽到該消息后調(diào)用WebViewJavascriptBridge._fetchQueue()去取數(shù)據(jù)
- 根據(jù)handlerName從messageHandlers中取出該callback;根據(jù)callbackId創(chuàng)建callback對象作為參數(shù)放到handlerName的方法中;執(zhí)行該回調(diào)。
總結(jié)
綜上,WebViewJavascriptBridge的核心流程就分析完了,最核心的點是JS通過加載iframe來通知native側(cè);native側(cè)通過evaluateJavaScript方法去執(zhí)行JS。
從整個SDK來看,設計的非常好,值得借鑒學習:
- 使用外觀模式統(tǒng)一調(diào)用接口,比如初始化WebViewJavascriptBridge的時候,不需要關心使用方使用的是UIWebView還是WKWebView,內(nèi)部已經(jīng)處理好了。
- 接口統(tǒng)一,不管是native側(cè)還是JS側(cè),調(diào)用方法就是callHandler、注冊方法就是registerHandler,不需要關注內(nèi)部實現(xiàn),使用非常方便。
- 代碼簡潔,邏輯清晰,層次分明。從類的分布就能很清晰的看出各自的功能是什么。
- 職責單一,比如decidePolicyForNavigationAction方法只負責監(jiān)聽事件、_fetchQueue是負責把消息轉(zhuǎn)換成JSON字符串返回、_doSend是發(fā)送消息到native、_dispatchMessageFromObjC是負責處理從OC返回的消息等。雖然decidePolicyForNavigationAction也能接收消息,但這樣就不會這么精簡了。
- 擴展性好,目前decidePolicyForNavigationAction雖然只有初始化和發(fā)消息兩個事件,如果有其他事件還可以再擴展,這也得益于方法設計的職責單一,擴展對原有方法影響會很小。
以上就是iOS開發(fā)WebViewJavascriptBridge通訊原理解析的詳細內(nèi)容,更多關于iOS WebViewJavascriptBridge通訊的資料請關注腳本之家其它相關文章!
相關文章
IOS AFNetworking的Post失敗及requestSerializer的正確使用
這篇文章主要介紹了IOS AFNetworking的Post失敗及requestSerializer的正確使用的相關資料,需要的朋友可以參考下2017-05-05
IOS 播放系統(tǒng)提示音使用總結(jié)(AudioToolbox)
這篇文章主要介紹了IOS 播放系統(tǒng)提示音使用總結(jié)(AudioToolbox)的相關資料,需要的朋友可以參考下2017-05-05

