- WebViewJavascriptBridge 原理分析
-
網上好多都是在介紹 WebViewJavascriptBridge如何使用,這篇文章就來說說 WebViewJavascriptBridge 設計原理。
主要從兩個過程來講一下:js調用UIViewController中的代碼(Native),Native調用js
1.概述
首先有兩個問題:
a.Native(中的UIWebView)是否可以直接調用js method(方法)? 可以。
b.js 是否可以直接調用Native的mthod?不行。
明確上述兩個問題,那么上圖就不難明白了,webpage中的js method和webview本地的method之間關系。那WebViewJavascriptBridge出現是否解決這個問題(這個問題就是讓js可以直接調用native的method)呢?答案是否定的?沒有本質還是用uiwebview的代理方法進行字段攔截(判斷url的scheme),實現js間接調用native的method。
我們來看WebViewJavascriptBridge提供的demo:
主要的核心是下面兩個,接下來我們就來討論一下其設計原理。
2.js調用Native method
在概述中說過,js是不能直接調用native的method所以,需要借助- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType,這個方法大家不陌生,每次在重新定向URL的時候,這個方法就會被觸發,通常情況,我們會在這里做一些攔截完成js和本地的間接交互什么的。那么WebViewJavascriptBridge也不另外,也是這么做。
我們先來看看在ExampleApp.html文件中點擊一個按鈕發起請求的代碼:
12345678910var callbackButton = document.getElementById(
'buttons'
).appendChild(document.createElement(
'button'
))
callbackButton.innerHTML =
'Fire testObjcCallback'
callbackButton.onclick = function(e) {
e.preventDefault()
log(
'JS calling handler "testObjcCallback"'
)
//1
bridge.callHandler(
'testObjcCallback'
, {
'foo'
:
'cccccccccccc'
}, function(response) {
log(
'JS got response'
, response)
})
}
估計大家大體都能看懂,唯獨有疑問的地方是:1234bridge.callHandler(
'testObjcCallback'
, {
'foo'
:
'cccccccccccc'
}, function(response) {
log(
'JS got response'
, response)
})
}
這段代碼先不說,上面代碼就是一個按鈕的普通單擊事件方法。我們一起想一下,如果這個按鈕需要被點擊之后調用native中的funtion函數,之后需要把這個(native的)funtion函數處理結果返回給js中的方法繼續處理。這個是我們需求,帶着這個需求我們看一下這個方法,testObjcCallBack這個我們猜測一下應該native中的方法或者一個能夠調用到方法的name/id,后面這個是個json{‘foo’:‘ccccccccccccc’},應該是個參數,那么后面這個方法一看log應該知道,是對native返回的result進行處理的方法。拿具體是不是呢?只要找到callHandler方法就知道了。在文件WebViewJavascriptBridge.js.txt里面我們找找這個方法:
123function callHandler(handlerName, data, responseCallback) {
_doSend({ handlerName:handlerName, data:data }, responseCallback)
}
這里又多了一個方法叫_doSend連個參數 第1個是字典key-value定義,第二個是一個方法的指針(看看上面的方法你就知道了),那我們必須在同一個文件里面看看能不能找到這個_doSend方法:123456789function _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
}
找到了。逐行分析一下,變量callbackId是個字符串,responseCallBacks[] 一看就知道是個字典 ,這個字典把回掉(我們猜測)的方法responseCallback給保存起來,這Key(也就是callbackId)應該是唯一的,通過計數和時間應該知道這個字符串應該是唯一的,message也是一個字典,這是給message添加了一個新的key-value。干嘛呢?我也不知道,我們來看看sendMessageQueue是什么,大家一個push就知道應該是個數組。他吧一個字典放到一個消息隊列中(數組隊列),讓后產生一個src(url scheme)。
有兩個變量我們看看:
12var CUSTOM_PROTOCOL_SCHEME =
'wvjbscheme'
var QUEUE_HAS_MESSAGE =
'__WVJB_QUEUE_MESSAGE__'
干嘛用,肯定是給webview 的 delegate判斷用的,你感覺呢?(肯定是)
下面是在文件:WebViewJavascriptBridge.m
好了到了這里大家猜猜這個要干嘛?肯定是要發url讓web截取對吧?那還用問啊,肯定是啊,已經說過了js能不能調用native的funtion函數?不能。我們來看看這個messagingIframe是:
123456function _createQueueReadyIframe(doc) {
messagingIframe = doc.createElement(
'iframe'
)
messagingIframe.style.display =
'none'
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +
'://'
+ QUEUE_HAS_MESSAGE
doc.documentElement.appendChild(messagingIframe)
}
原來就是iframe,這個就不同給大家解釋了。好了src一產生就會出現什么,uiwebview代理回掉截獲,此時我們把目光回到UIWebview的Native下面:123456789101112131415161718192021222324252627- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if
(webView != _webView) {
return
YES; }
NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if
([[url scheme] isEqualToString:kCustomProtocolScheme])
{
if
([[url host] isEqualToString:kQueueHasMessage])
{
//會走這里
[self _flushMessageQueue];
}
else
{
NSLog(@
"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@"
, kCustomProtocolScheme, [url path]);
}
return
NO;
}
else
if
(strongDelegate && [strongDelegate respondsToSelector:
@selector
(webView:shouldStartLoadWithRequest:navigationType:)])
{
return
[strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
}
else
{
return
YES;
}
}
一看就頭大,哈哈,是,我也頭大。看看上面的注釋說 會走這里,我們看看為什么會走那里,最外圈的if([url scheme])判斷是#define kCustomProtocolScheme @"wvjbscheme"
這個定義是什么意思,我們先不做解釋,剛才我們說過js不能直接調用native的function,大家只要記住這點,接着往下走就是了。至於為什么走這里,自己看代碼(上文有提到),我們看看_flushMessageQueue:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253- (
void
)_flushMessageQueue {
NSString *messageQueueString = [_webView stringByEvaluatingJavaScriptFromString:@
"WebViewJavascriptBridge._fetchQueue();"
];
//json轉成數組
id messages = [self _deserializeMessageJSON:messageQueueString];
if
(![messages isKindOfClass:[NSArray
class
]]) {
NSLog(@
"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@"
, [messages
class
], messages);
return
;
}
for
(WVJBMessage* message in messages) {
if
(![message isKindOfClass:[WVJBMessage
class
]]) {
NSLog(@
"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@"
, [message
class
], message);
continue
;
}
[self _log:@
"RCVD"
json:message];
//用於js回掉
NSString* responseId = message[@
"responseId"
];
if
(responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@
"responseData"
]);
[_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;
if
(message[@
"handlerName"
]) {
handler = _messageHandlers[message[@
"handlerName"
]];
}
else
{
handler = _messageHandler;
}
if
(!handler) {
[NSException raise:@
"WVJBNoHandlerException"
format:@
"No handler for message from JS: %@"
, message];
}
handler(message[@
"data"
], responseCallback);
}
}
}
這下牛逼了,不忍直視啊!這么多,哈哈,多不可怕,可怕是你堅持不下去了。我們逐行來看:
NSString *messageQueueString = [_webView stringByEvaluatingJavaScriptFromString:@"WebViewJavascriptBridge._fetchQueue();"];
我們必須回去到js文件中去,這里是webview直接調用js中的方法:
12345function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue)
sendMessageQueue = []
return
messageQueueString
}
謝天謝地這個方法代碼不多,這個消息很眼熟,SendMessageQueue,剛才我們說什么來?他是一個字典,那里面有哪些東西,我么來看看handlerName:handlerName,
data:data,
callbackId:callbackId
這個消息字典此時被取出來准備做什么,這里提示下我們已經走到webview 的delegate里面了,所以拿到這些信息肯定是調用native的method對吧?肯定是的。接着往下走,接着會把json字符串轉成數組,然后進行判斷,
1NSString* responseId = message[@
"responseId"
];
1234567891011121314151617181920212223242526272829WVJBResponseCallback 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;
if
(message[@
"handlerName"
]) {
handler = _messageHandlers[message[@
"handlerName"
]];
}
else
{
handler = _messageHandler;
}
if
(!handler) {
[NSException raise:@
"WVJBNoHandlerException"
format:@
"No handler for message from JS: %@"
, message];
}
handler(message[@
"data"
], responseCallback);
123- (
void
)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_messageHandlers[handlerName] = [handler copy];
}
那又是誰調用了這個方法:找到了(在文件 ExampleAppViewController.m的viewdidload中),這里有方法testObjecCallback
1234[_bridge registerHandler:@
"testObjcCallback"
handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@
"testObjcCallback called: %@"
, data);
responseCallback(@
"Response from testObjcCallback"
);
}];
有點亂了。剛才我們的思路都是倒推的,如果我們整過來,首先肯定是viewdidload初始化,初始化之后會把這個block加入到_messageHandlers中,之后因為js調用動態讀取這個block調用,在調用之前,我們又把定一個block付給回掉處理的responseCallback的block,這個block在handler中調用而調用,有點繞,自己可以多想想。我們接着來看看:
12345678responseCallback = ^(id responseData) {
if
(responseData == nil) {
responseData = [NSNull
null
];
}
WVJBMessage* msg = @{ @
"responseId"
:callbackId, @
"responseData"
:responseData };
[self _queueMessage:msg];
};
這個就是你繞的地方,他是后被定義的,所以一開不執行,只有在處理數據后回調才會被調用,這里有個方法_queueMessage:1234567- (
void
)_queueMessage:(WVJBMessage*)message {
if
(_startupMessageQueue) {
[_startupMessageQueue addObject:message];
}
else
{
[self _dispatchMessage:message];
}
}
這里面還有個方法:12345678910111213141516171819202122- (
void
)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message];
[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];
if
([[NSThread currentThread] isMainThread]) {
[_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
}
else
{
__strong WVJB_WEBVIEW_TYPE* strongWebView = _webView;
dispatch_sync(dispatch_get_main_queue(), ^{
[strongWebView stringByEvaluatingJavaScriptFromString:javascriptCommand];
});
}
}
我們在回到WebViewJavascriptBridge.js.txt文件中看到
1234567function _handleMessageFromObjC(messageJSON) {
if
(receiveMessageQueue) {
receiveMessageQueue.push(messageJSON)
}
else
{
//肯定走這個 為什么呢?
_dispatchMessageFromObjC(messageJSON)
}
}
再來看看:123456789101112131415161718192021222324252627282930313233343536function _dispatchMessageFromObjC(messageJSON) {
setTimeout(function _timeoutDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON)
var messageHandler
var responseCallback
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({ responseId:callbackResponseId, responseData:responseData })
}
}
var handler = WebViewJavascriptBridge._messageHandler
if
(message.handlerName) {
handler = messageHandlers[message.handlerName]
}
try
{
handler(message.data, responseCallback)
}
catch
(exception) {
if
(typeof console !=
'undefined'
) {
console.log(
"WebViewJavascriptBridge: WARNING: javascript handler threw."
, message, exception)
}
}
}
})
}
大家還記得我們返回的對象是:1@{ @
"responseId"
:callbackId, @
"responseData"
:responseData }
所以這里messageHandlers剛才也說過了用來存方法的,callbackId被換了個名字叫responseId意思一樣,只要值沒變就行,所以就會執行:
123bridge.callHandler(
'testObjcCallback'
, {
'foo'
:
'cccccccccccc'
}, function(response) {
log(
'JS got response'
, response)
})
中的方法,好了,完了。總結一下:js這邊 先把方法名字、參數、處理方法保存成一個字典在轉成json字符串,在通過UIWebview調用js中某個方法把這個json字符串傳到Native中去(不是通過url傳的,這樣太low了),同時把這個處理的方法以key-value形式放到一個js的字典中。
UIWebView在收到這個json之后,進行數據處理、還有js的回掉的處理方法(就是那個callbackId)處理完成后也會拼成一個key-value字典通過調用js傳回去(可以直接調用js)。
js在接到這個json后,根據responseId讀取responseCallbacks中處理方法進行處理Native code返回的數據。
3.Native調用js method
過程不是直接調用js,也是通過js調用Native過程一樣的處理方式。
大體來看一下,先看一個按鈕的單擊事件:
123456- (
void
)callHandler:(id)sender {
id data = @{ @
"greetingFromObjC"
: @
"Hi there, JS!"
};
[_bridge callHandler:@
"testJavascriptHandler"
data:data responseCallback:^(id response) {
NSLog(@
"testJavascriptHandler responded: %@"
, response);
}];
}
看看callHandler:
123- (
void
)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[self _sendData:data responseCallback:responseCallback handlerName:handlerName];
}
看看_sendData:123456789101112131415161718- (
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];
_responseCallbacks[callbackId] = [responseCallback copy];
message[@
"callbackId"
] = callbackId;
}
if
(handlerName) {
message[@
"handlerName"
] = handlerName;
}
[self _queueMessage:message];
}
到_queueMessage:之后流程就和上面一樣了,這里面native也有個:123456NSString* responseId = message[@
"responseId"
];
if
(responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@
"responseData"
]);
[_responseCallbacks removeObjectForKey:responseId];
}
這個和js中的處理思想是一樣的。總結:native將方法名、參數、回到的id放到一個對象中傳給js。
js根據方法名字調用相應方法,之后將返回數據和responseId拼裝,最后通過src 重定向到UIWebview 的delegate。
native得到數據后根據responseId調用事先裝入_responseCallbacks的block,動態讀取調用,從而完成交互。