一. 概述
做客戶端開發免不了要與WebView
打交道,特別是對于Hybrid App
,在H5
所占比重越來越大的背景下,一套好的WebView
與原生交互的API顯得尤為重要,當然目前兩端都有比較成熟的三方庫進行支持。比如Android端的JsBridge,iOS端的WebViewJavascriptBridge,但是對于其內部原理筆者一直一知半解,導致有時面對問題無從下手,最后決心分析WebViewJavascriptBridge
的內部實現原理,一是提升自己的源碼閱讀水平,其次也希望對以后的工作有所幫助。
二. 基本原理
下載WebViewJavascriptBridge
的源碼后可以看到其文件并不多,分別對幾個文件做簡單的介紹,后面詳細分析其源碼
-
WebViewJavascriptBridge_JS
: JS橋接文件,通過它實現JS環境的初始化,里面就一個C函數,返回的是JS方法。原生調用的JS方法與對應的方法回調都需要先在這里面進行注冊。 -
WKWebViewJavascriptBridge
與WebViewJavascriptBridge
:WKWebView
與UIWebView
對應的橋接文件。JS調用的原生方法與對應的方法回調都需要先在這里面進行注冊。 -
WebViewJavascriptBridgeBase
: 橋接基礎文件。通過他實現對原生環境的初始化,以及對方法存儲容器的初始化,當然還有對WebViewJavascriptBridge_JS
里面JS方法的調用。
三. 源碼解析
大體了解了上面幾個類的作用,我們通過源碼來分析其內部的實現邏輯。我們就以WebViewJavascriptBridge
Demo為例。
1. JS調用OC方法
(1) OC環境初始化與方法注冊
如何實現JS調用OC方法呢,首先要對當前OC環境進行初始化
// ExampleWKWebViewController
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
....
// WKWebViewJavascriptBridge
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
....
// WKWebViewJavascriptBridge
- (void) _setupInstance:(WKWebView*)webView {
_webView = webView;
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
[WebViewJavascriptBridge bridgeForWebView:webView];
: 看這個方法的調用棧,可以清晰的看到其作用是初始化WKWebViewJavascriptBridge
,進而實例化其對應的WebViewJavascriptBridgeBase
,還有綁定各自的代理,最終實現初始化OC調用環境的目的。- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
: 如果要實現JS調用原生方法的目的,那么必須對原生方法進行注冊,這個就是對應的注冊方法。我們來看他內部做了什么:
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
很簡單,只不過把當前的Block保存進了messageHandlers
這個字典中,以便等JS端調用時,通過方法名稱來找到其對應的實現。
(2) JS環境初始化與方法觸發
OC環境初始化與方法注冊完成后,我們來下JS環境的初始化 Demo中通過- (void)loadExamplePage:(WKWebView*)webView
方法加載網頁到當前的webView,來看下ExampleApp.html
中的核心方法:
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
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)
}
setupWebViewJavascriptBridge(function(bridge) {
var uniqueId = 1
function log(message, data) {
var log = document.getElementById('log')
var el = document.createElement('div')
el.className = 'logLine'
el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
if (log.children.length) { log.insertBefore(el, log.children[0]) }
else { log.appendChild(el)}
}
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
})
document.body.appendChild(document.createElement('br'))
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback' callbackButton.onclick = function(e) {
e.preventDefault()
log('JS calling handler "testObjcCallback"')
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}
})
-
setupWebViewJavascriptBridge(callback)
是核心方法,webView加載html后會首先調用這個方法。這個方法需要一個參數callback
,也是一個函數。我們來看這個方法:
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
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)
}
第一次加載網頁時 window.WebViewJavascriptBridge
與 window.WVJBCallbacks
都是false,把window.WVJBCallbacks
賦值為包含callback的數組,此時callback為一個函數,就是后面的function(bridge) ....
,接下來創建WVJBIframe
,你可以把它理解為一個空白頁面,創建它的目的是設置src = 'https://__bridge_loaded__';
,
注意這個
src
屬性很關鍵,當我們設置一個網頁的src
屬性時,這個鏈接會被我們OC端的webView所捕獲,從而調用webView的代理方法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
,
后面兩句代碼的的意思是加載當前空白頁,以便觸發OC的代理方法,然后立馬移除。
- 接下來我們去
WKWebViewJavascriptBridge
中看- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
這個代理方法攔截到請求后做了什么。
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
首先判斷當前的URL是否是__wvjb_queue_message__
或者__bridge_loaded__
,剛才觸發的URL是 __bridge_loaded__
會調用WebViewJavascriptBridgeBase
的- (void)injectJavascriptFile
方法。
- (void)injectJavascriptFile {
// 獲取JS字符串
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
....
- (void) _evaluateJavascript:(NSString *)javascriptCommand {
[self.delegate _evaluateJavascript:javascriptCommand];
}
....
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
return NULL;
}
通過以上方法調用可以看到,最后是把WebViewJavascriptBridge_js();
JS方法字符串,通過方法 [_webView evaluateJavaScript:javascriptCommand completionHandler:nil]
注入到了webView中并且執行。從而達到初始化javascript環境的brige的作用。
- WebViewJavascriptBridge_js()方法解析
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
var messagingIframe;
// 要發送給原生的消息列表
var sendMessageQueue = [];
// 存儲注冊在bridge的JS方法
var messageHandlers = {};
// 要跳轉的
URLvar CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
//JS方法回調
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;
// OC調用的JS方法需要用它來進行注冊
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
//JS調用OC的方法入口function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
}
// 要發送消息給原生了
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
//觸發webView 代理,解析JS 的message messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
// 把消息轉成json字符串function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
function _dispatchMessageFromObjC(messageJSON) {
....
}
// 原生會調用他,JS用它來達到消息分發
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
我截選了一些關鍵代碼,首先整個WebViewJavascriptBridge_js
是一個JS方法的執行,首先創建了JS端的WebViewJavascriptBridge
并賦值給了window
,我們來看這個對象的構成:
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
-
registerHandler
:直接對應下面的registerHandler(handlerName, handler)
方法,通過它我們把能被OC調用的JS方法進行注冊,看它的實現也是比較簡單的
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
把JS的方法實現以方法名handleName
保存在messageHandlers中。
-
callHandler
: 對應下面callHandler(handlerName, data, responseCallback)
方法,通過它我們可以直接發起對OC方法的調用,具體調用邏輯我們在下面進行分析。 -
disableJavscriptAlertBoxSafetyTimeout
:回調是否超時開關,默認為false -
_fetchQueue
: 把javascript環境的方法序列化成JSON字符串,并返回給OC端 -
_handleMessageFromObjC
:處理OC發給javascript環境的方法,_dispatchMessageFromObjC(messageJSON)
這個方法的參數就是OC調用JS的message信息,這個方法對messageJSON進行解析處理,進而調用相應的JS方法。
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
這里的src就是https://wvjb_queue_message,這段代碼的意思是把javascript要發送給OC的消息立即發送出去。
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
WebViewJavascriptBridge_js
的最后是上面的代碼,它會調用ExampleApp.html
中的callBack方法,也就是它
setupWebViewJavascriptBridge(function(bridge) {
....
})
繼而完成對這個JS環境的初始化與ExampleApp.html
的加載。
(3) JS調用OC方法流程
- 點擊JS按鈕觸發下面的方法
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
傳遞方法名testObjcCallback
,消息參數{'foo': 'bar'}
,以及OC回調JS的方法function(response) {log('JS got response', response)})
,
- 調用
WebViewJavascriptBridge_js
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
....
function _doSend(message, responseCallback) {
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;
}
可以看到核心方法是_doSend()
,入參message是調用OC的方法名與參數,responseCallback是OC回調JS的方法,接下來將這個回調方法保存在responseCallbacks中,key值是callbackId
,消息對象message也添加一個callbackId
,最后設置messagingIframe
的src屬性,從而被ebView的代理方法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
攔截。
- 在上面的代理方法中,攔截到的URL為
__wvjb_queue_message__
,所以調用方法:
- (void)WKFlushMessageQueue {
[_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
[_base flushMessageQueue:result];
}];
}
....
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
WebView觸發JS的WebViewJavascriptBridge._fetchQueue()
,
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
這個方法里面會將sendMessageQueue
換成json字符串,然后返回給OC環境,觸發[_base flushMessageQueue:result];
- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}
// JS傳遞過來的json字符串,我們進行反序列化 得到message數組
NSLog(@"messageQueueString===%@",messageQueueString);
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
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"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
//向下傳遞參數,并且觸發block回調()
handler(message[@"data"], responseCallback);
}
}
}
這個方法就是OC端處理JS的核心方法了,將messageQueueString
反序列化,得到消息數組
(
{
callbackId = "cb_1_1639553291614";
data = {
foo = bar;
};
handlerName = testObjcCallback;
}
)
有callbackId
,表明有消息回調,生成responseCallback
Block,這個Block里面將接收的參數與callbackId
打包一并發送給JS環境,并調用JS環境的WebViewJavascriptBridge._handleMessageFromObjC(messageJSON);
方法將messageJSON
進行解析。這里只是Block的實現,并沒調用這個Block,調用在下面
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
//向下傳遞參數,并且觸發block回調()
handler(message[@"data"], responseCallback);
根據handlerName
找到在messageHandlers
保存的方法實現,handler(message[@"data"], responseCallback);
進行真正的調用,在OC的注冊方法中,調用responseCallback(@"Response from testObjcCallback");
向JS環境發送回調并傳遞參數。
- JS環境通過這個
_handleMessageFromObjC(messageJSON)
方法得到messageJSON
,并對其解析。
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
//轉換為對象
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 這個responseId就是JS調用OC方法保存的callbackId
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);
}
}
}
}
先將字符串轉化為JSON對象,根據responseId(這個responseId就是JS調用OC方法保存的callbackId),找到對應的方法實現,進行調用
function(response) {
log('JS got response', response)
}
到此就完成了JS調用OC,并且OC回調JS并傳遞參數的全部過程。
2. OC調用JS方法
跟上面類似再來看下OC主動調用JS方法的實現
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
....
- (void)callHandler:(NSString *)handlerName data:(id)data {
[self callHandler:handlerName data:data responseCallback:nil];
}
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
....
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
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];
}
....
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
NSLog(@"javascriptCommand==%@",javascriptCommand);
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
可以看到,跟JS調用OC方法的原理類似,將OC調用JS的方法名與參數封裝進message對象,如果有回調函數,將回調函數通過responseCallbacks
保存,并生成callbackId
,將整個message打包發送給JS環境的WebViewJavascriptBridge._handleMessageFromObjC(messageJSON);
進行解析,解析流程上面介紹過了,這里不再贅述。
四. 總結
通過上面流程分析,整個WebViewJavascriptBridge
內部的實現原理就比較清晰了。
- JS將方法注冊到JS環境的bridge,OC調用JS的核心方法就是
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
,JS環境收到消息后,通過方法WebViewJavascriptBridge._handleMessageFromObjC();
將消息進行解析,調用注冊在bridge的方法。 - OC將方法注冊到OC環境的bridge,JS調用OC的核心邏輯是,設置空白網頁的
src
屬性,從而被webView的代理方法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
,OC通過核心方法- (void)flushMessageQueue:(NSString *)messageQueueString
將傳遞數據進行解析。 - 兩邊都是通過方法名找到對應的方法實現,然后通過ID來查找回調函數。