博客鏈接 WebViewJavascriptBridge源碼分析
在APP的開發(fā)過程中,都會(huì)通過H5來實(shí)現(xiàn)部分功能,H5頁面是內(nèi)嵌在原生應(yīng)用的WebView組件中吞鸭。在有的場(chǎng)景下氯哮,當(dāng)兩端需要相互通信稍计,但是JavaScript的權(quán)限受到限制岸蜗,比如不能修改系統(tǒng)配置等,這個(gè)時(shí)候需要委托Native去實(shí)現(xiàn)某個(gè)功能重罪,并在完成后將結(jié)果通知JavaScript努隙。所以我們需要在Native和JavaScript之間就搭建一個(gè)通信的橋梁球恤,這個(gè)橋梁就是我們所說的JavaScript Bridge,簡稱 JS Bridge荸镊。
通常實(shí)現(xiàn)Native與JS橋接的方式有兩種:
- 通過JavaScriptCore框架
- 通過Webview攔截請(qǐng)求的方式(WebViewJavascriptBridge使用的方式)
marcuswestin/WebViewJavascriptBridge是使用第2種方式實(shí)現(xiàn)在用于在WKWebView和UIWebView中咽斧,JS與Native相互發(fā)送消息。
與其他OC的三方庫不同躬存,WebViewJavascriptBridge的實(shí)現(xiàn)包括OC和JS兩部分张惹,因此只看OC部分的代碼我們是無法理解這個(gè)bridge是如何實(shí)現(xiàn)兩端通信的。
WebViewJavascriptBridge中的類的作用
-
WebViewJavascriptBridgeBase
:OC端橋接基礎(chǔ)服務(wù)類岭洲,維護(hù)OC端開放給JS端的方法以及OC回調(diào)方法宛逗,實(shí)現(xiàn)OC向JS發(fā)送數(shù)據(jù)的具體邏輯。 -
WebViewJavascriptBridge_JS
:維護(hù)了一份JS代碼盾剩,用于JS環(huán)境的注入雷激。同時(shí)維護(hù)JS端的bridge對(duì)象替蔬,管理JS端注冊(cè)的方法集合以及回調(diào)方法集合,面向Web端提供注冊(cè)JS方法屎暇、調(diào)用OC端方法的接口进栽。 -
WKWebViewJavascriptBridge
:基于WKWebView的OC端交互邏輯處理類,面向OC業(yè)務(wù)層恭垦,提供了注冊(cè)O(shè)C方法、調(diào)用JS方法等接口格嗅。 -
WebViewJavascriptBridge
:基于UIWebView的的OC端交互邏輯處理類番挺,與WKWebViewJavascriptBridge
的功能一致。
WebViewJavascriptBridge源碼解析
WebViewJavascriptBridge
的實(shí)現(xiàn)可以說是雙向的過程屯掖,無論是JS端還是Native端都包含以下三部分內(nèi)容:
- bridge初始化
- 本端注冊(cè)函數(shù)共另一端調(diào)用
- 調(diào)用另一端函數(shù)
目前App已經(jīng)取消對(duì)UIWebView的支持玄柏,所以我們只需要看WKWebView相關(guān)部分的實(shí)現(xiàn)即可。
bridge初始化
bridge初始化分為Native初始化bridge和JS初始化bridge贴铜。在使用WebView的時(shí)候粪摘,都是從Native端打開頁面開始,因此先分析Native初始化bridge绍坝。
Native初始化bridge
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
- (void)_setupInstance:(WKWebView*)webView {
_webView = webView;
// 將webView的navigationDelegate設(shè)為WKWebViewJavascriptBridge對(duì)象自身
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
// WKWebViewJavascriptBridge對(duì)象需要實(shí)現(xiàn)_evaluateJavascript:代理方法
_base.delegate = self;
}
- (void)reset {
[_base reset];
}
WKWebViewJavascriptBridge
中_evaluateJavascript:
方法的實(shí)現(xiàn):
- (NSString*)_evaluateJavascript:(NSString*)javascriptCommand {
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
return NULL;
}
關(guān)于JS代碼的注入徘意,可以使用WKUserContentController
,也可以使用evaluateJavaScript:completionHandler:
這個(gè)函數(shù)轩褐,WebViewJavascriptBridge
使用后者實(shí)現(xiàn)JS注入椎咧,因此需要將webView的navigationDelegate
設(shè)為WKWebViewJavascriptBridge
對(duì)象自身,并在代理方法中調(diào)用evaluateJavaScript:completionHandler:
函數(shù)把介。
在Native初始化后勤讽,就要使用load
之類的方法加載頁面與JS代碼,這進(jìn)入到JS初始化bridge過程拗踢。
JS初始化bridge
相對(duì)于Native初始化bridge來說脚牍,JS初始化bridge就要顯得難一些。
<script>
function setupWebViewJavascriptBridge(callback) {
// window表示瀏覽器窗口
// WebViewJavascriptBridge就是bridge對(duì)象巢墅。
// 如果有bridge對(duì)象則直接調(diào)用callback并傳入bridge對(duì)象诸狭。
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
// 如果有WVJBCallbacks則將回調(diào)函數(shù)push到數(shù)組里,后面初始化bridge時(shí)會(huì)統(tǒng)一遍歷調(diào)用callback砂缩,并傳入bridge作谚。
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
// 網(wǎng)頁會(huì)請(qǐng)求這個(gè)鏈接
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
// setTimeout是Native端注入的一個(gè)函數(shù)
setTimeout(function () { document.documentElement.removeChild(WVJBIframe) }, 0)
}
// 執(zhí)行調(diào)用setupWebViewJavascriptBridge函數(shù),bridge就是對(duì)象庵芭。
// 在WebViewJavascriptBridge_JS.m文件中對(duì)bridge的定義
// window.WebViewJavascriptBridge = {
// registerHandler: registerHandler,
// callHandler: callHandler,
// disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
// _fetchQueue: _fetchQueue,
// _handleMessageFromObjC: _handleMessageFromObjC
// };
setupWebViewJavascriptBridge(function (bridge) {
// ...
// JS注冊(cè)函數(shù)供Native調(diào)用
bridge.registerHandler('testJavascriptHandler', function (data, responseCallback) {
// ...
})
// ...
})
</script>
在JS初始化bridge過程中妹懒,會(huì)直接調(diào)用setupWebViewJavascriptBridge(callback)
函數(shù),callback相當(dāng)于block/閉包双吆。在這個(gè)過程中眨唬,callHandler
会前、registerHandler
等函數(shù)都是通過JS代碼注入的,這些代碼都在Native端匾竿,那它是如何成功執(zhí)行這些函數(shù)的呢瓦宜?關(guān)鍵在于https://__bridge_loaded__
這個(gè)url。在使用了這個(gè)url后岭妖,WKWebView的NavigationDelegate會(huì)攔截這個(gè)請(qǐng)求临庇,并注入JS代碼。相關(guān)實(shí)現(xiàn)如下:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
// ...
// isBridgeLoadedURL函數(shù)中的kBridgeLoaded即為__bridge_loaded__
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
}
// ...
}
用泳道圖來描述初始化bridge的過程
JS注冊(cè)函數(shù)昵慌,Native調(diào)用JS
JS注冊(cè)函數(shù)
// JS注冊(cè)函數(shù)給Native調(diào)用
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
var responseData = { 'Javascript Says':'Right back atcha!' }
responseCallback(responseData)
})
// WebViewJavascriptBridge_JS.m中的JS代碼
// 保存JS函數(shù)與函數(shù)名的映射關(guān)系
var messageHandlers = {};
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
Native調(diào)用JS函數(shù)
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
內(nèi)部調(diào)用了WebViewJavascriptBridgeBase
的sendData:responseCallback:handlerName:
方法假夺。
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];
// JS函數(shù)所需的參數(shù)
if (data) message[@"data"] = data;
// responseCallback:Native調(diào)用JS函數(shù)后的回調(diào)函數(shù)
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
// 保存Native回調(diào)
self.responseCallbacks[callbackId] = [responseCallback copy];
// 保存回調(diào)方法的id
message[@"callbackId"] = callbackId;
}
// JS函數(shù)名
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}
在Native調(diào)用JS的函數(shù)時(shí),有時(shí)Native需要JS調(diào)用Native的回調(diào)函數(shù)返回一些數(shù)據(jù)斋攀,因此需要保存回調(diào)函數(shù)的一些信息已卷,關(guān)于Native的回調(diào)函數(shù)是如何調(diào)用的,在后面會(huì)講到淳蔼。
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
在開發(fā)過程中侧蘸,有可能Native調(diào)用JS函數(shù)的時(shí)候,JS端還沒有完成bridge準(zhǔn)備工作鹉梨。bridge是在decidePolicyForNavigationAction:
的代理方法中執(zhí)行injectJavascriptFile
方法才完成的讳癌,但是
callHandler
可能在viewWillAppear
的時(shí)候調(diào)用,此時(shí)沒有完成JS端bridge的初始化俯画,所以先存入startupMessageQueue
中析桥,等準(zhǔn)備完成后, 再統(tǒng)一調(diào)用 startupMessageQueue
中的Message到JS艰垂,并將startupMessageQueue
置為空泡仗。
- (void)injectJavascriptFile {
// 注入JS bridge的環(huán)境代碼
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
// 對(duì)于一些提前調(diào)用的callHandler,在注入JS初始化代碼后猜憎,會(huì)統(tǒng)一發(fā)送娩怎,并清空startupMessageQueue
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
- (void)_dispatchMessage:(WVJBMessage*)message {
// 序列化message
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
// 省略對(duì)messageJSON的處理
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
_dispatchMessage
將之前拼裝好的message傳給JS,用JS bridge的 _handleMessageFromObjC
函數(shù)處理Native的調(diào)用請(qǐng)求胰柑,_handleMessageFromObjC
函數(shù)是在JS bridge初始化的時(shí)候注入的截亦。
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
function _dispatchMessageFromObjC(messageJSON) {
// 忽略dispatchMessagesWithTimeoutSafety部分
_doDispatchMessageFromObjC();
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 是否有responseId,對(duì)于Native調(diào)用JS函數(shù)所傳過來的message來說是沒有該字段的
// if (message.responseId) {
// // ...
// }
// 處理Native調(diào)用JS函數(shù)的message
if (message.callbackId) {
// 是否含有Native回調(diào)
var callbackResponseId = message.callbackId;
responseCallback = function (responseData) {
// _doSend函數(shù)只傳了message柬讨,另外沒有responseCallback參數(shù)
_doSend({
handlerName: message.handlerName,
// 如果Native傳過來的message有回調(diào)崩瓤,那么JS端需要傳入一個(gè)responseId,這樣Native端才能通過responseId這個(gè)key
// 在responseCallbacks字典中找到對(duì)應(yīng)的Native回調(diào)
responseId: callbackResponseId,
responseData: responseData
});
};
}
// 通過handlerName獲取到對(duì)應(yīng)的JS函數(shù)踩官,并調(diào)用却桶。
// messageHandlers保存了JS bridge的函數(shù)名和回調(diào)函數(shù)
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
// 這個(gè)_doSend是精簡之后的實(shí)現(xiàn),_doDispatchMessageFromObjC中的_doSend函數(shù)沒有傳遞responseCallback參數(shù)
function _doSend(message) {
// sendMessageQueue保存message信息,這個(gè)message信息是給Native回調(diào)時(shí)候用的
sendMessageQueue.push(message);
// src = https://__wvjb_queue_message__颖系,WKWebView的代理方法優(yōu)惠攔截這個(gè)url嗅剖,從而調(diào)用WKWebViewJavascriptBridge的KFlushMessageQueue方法
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
當(dāng)WKWebView的代理方法攔截到https://__wvjb_queue_message__
這個(gè)url的時(shí)候,就會(huì)調(diào)用WKFlushMessageQueue
方法
if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
}
- (void)WKFlushMessageQueue {
[_webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
NSLog(@"%@", result);
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
[_base flushMessageQueue:result];
}];
}
執(zhí)行_fetchQueue()
這個(gè)JS函數(shù)嘁扼,并在completionHandler
這個(gè)block內(nèi)返回JS中sendMessageQueue
的信息信粮,從而獲取帶responseId
的message。接著執(zhí)行Native的flushMessageQueue
方法趁啸。
// 在flushMessageQueue方法中完成了Native調(diào)用JS函數(shù)后的回調(diào)
- (void)flushMessageQueue:(NSString *)messageQueueString{
// 省略messageQueueString的有效性判斷
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
// 省略對(duì)Message類型的校驗(yàn)
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
}
}
}
用泳道圖來描述Native調(diào)用JS的過程
Native注冊(cè)函數(shù)强缘,JS調(diào)用Native
Native注冊(cè)函數(shù)
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
//
responseCallback(@"Response from testObjcCallback");
}];
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
// messageHandlers用來保存OC函數(shù)與函數(shù)名的映射關(guān)系
_base.messageHandlers[handlerName] = [handler copy];
}
JS調(diào)用Native函數(shù)
bridge.callHandler('testObjcCallback', { 'foo': 'bar' }, function (response) {
log('JS got response', response)
})
// JS端callHandler的實(shí)現(xiàn)
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
在Native調(diào)用JS的過程也使用了_doSend
函數(shù),它的作用是為了能調(diào)用Native調(diào)用JS函數(shù)之后的回調(diào)函數(shù)不傅。在JS調(diào)用Native的過程中欺旧,_doSend
函數(shù)是為了調(diào)用OC函數(shù)(與函數(shù)名對(duì)應(yīng)的block),responseCallback則是代表JS調(diào)用OC函數(shù)后的回調(diào)函數(shù)蛤签。
function _doSend(message, responseCallback) {
// 如果有JS回調(diào),則使用responseCallbacks保存JS回調(diào)
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
之后的邏輯在OC調(diào)用JS中已經(jīng)描述過了栅哀,直到執(zhí)行flushMessageQueue:
方法前都是一樣的
這里就不再重復(fù)震肮。接著看一下flushMessageQueue:
方法在JS調(diào)用Native過程中的實(shí)現(xiàn):
- (void)flushMessageQueue:(NSString *)messageQueueString{
// 省略messageQueueString的有效性判斷
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
// 省略對(duì)Message類型的校驗(yàn)
// 忽略關(guān)于responseId的實(shí)現(xiàn)
// 對(duì)于JS調(diào)用Native所傳過來的message來說是沒有responseId字段的
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
// 判斷是否有JS回調(diào)
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
// _queueMessage: -> _dispatchMessage: -> JS: _handleMessageFromObjC -> JS: _dispatchMessageFromObjC
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
// 通過handlerName獲取到對(duì)應(yīng)的Native函數(shù),并調(diào)用留拾。
// messageHandlers保存了Native bridge的函數(shù)名和回調(diào)函數(shù)
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
// 執(zhí)行Native函數(shù)
handler(message[@"data"], responseCallback);
}
}
關(guān)于_queueMessage:
方法前面已經(jīng)分析過戳晌,這里就不再重復(fù),接著看一下_dispatchMessageFromObjC
函數(shù)在JS調(diào)用Native過程中的實(shí)現(xiàn):
// 精簡了_doDispatchMessageFromObjC痴柔,只保留調(diào)用JS回調(diào)的部分
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var responseCallback;
// 如果有JS回調(diào)沦偎,那么OC傳過來的message必然存在responseId字段
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
// 調(diào)用JS回調(diào)
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
}
}
接著用泳道圖來描述下JS調(diào)用Native的過程
NNWKWebViewJSBridge
NNWKWebViewJSBridge是我在了解WebViewJavascriptBridge的實(shí)現(xiàn)過程后,基于這個(gè)項(xiàng)目咳蔚,實(shí)現(xiàn)一個(gè)輕量級(jí)Swift版本JSBridge豪嚎,并且它僅需要支持WKWebView即可。
相對(duì)于WebViewJavascriptBridge谈火,我使用了WKUserContentController
簡化了初始化和消息傳遞的實(shí)現(xiàn)過程侈询,相對(duì)來說會(huì)更好理解,消息傳遞性能也要比攔截Requests的方式要高糯耍。
項(xiàng)目地址:NNWKWebViewJSBridge
項(xiàng)目截圖: