- 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"')//1bridge.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++)+'_'+newDate().getTime()responseCallbacks[callbackId] = responseCallbackmessage['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_MESSAGEdoc.documentElement.appendChild(messagingIframe)}
原來就是iframe,這個就不同給大家解釋了。好了src一產生就會出現什么,uiwebview代理回掉截獲,此時我們把目光回到UIWebview的Native下面:123456789101112131415161718192021222324252627- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {if(webView != _webView) {returnYES; }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]);}returnNO;}elseif(strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]){return[strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];}else{returnYES;}}
一看就頭大,哈哈,是,我也頭大。看看上面的注釋說 會走這里,我們看看為什么會走那里,最外圈的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:[NSArrayclass]]) {NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messagesclass], messages);return;}for(WVJBMessage* message in messages) {if(![message isKindOfClass:[WVJBMessageclass]]) {NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messageclass], 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 = [NSNullnull];}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 = []returnmessageQueueString}
謝天謝地這個方法代碼不多,這個消息很眼熟,SendMessageQueue,剛才我們說什么來?他是一個字典,那里面有哪些東西,我么來看看handlerName:handlerName,
data:data,
callbackId:callbackId
這個消息字典此時被取出來准備做什么,這里提示下我們已經走到webview 的delegate里面了,所以拿到這些信息肯定是調用native的method對吧?肯定是的。接着往下走,接着會把json字符串轉成數組,然后進行判斷,
有沒有responseid,你說又沒,肯定沒有啊(你不行看看上面),所以就這這里了1NSString* responseId = message[@"responseId"];這部分是重點,到底他是怎么要調用本地function的,callbackId大家熟悉吧,判斷是否為空,不為空給他指定一個block,這個不說了,block指定,此時不調用(手動調用才會執行),這個剛才說了用來處理native的function處理的result用於把處理后的值返回給js的,接着往下去,看到handler這個方法會從message找到handlerName,這里我們看一下多了一個_messageHandlers字典,從這個字典獲取一個block(WVJBHandler是一個block),直接執行了。那我們看看_messageHandlers是怎么被添加block的:1234567891011121314151617181920212223242526272829WVJBResponseCallback responseCallback = NULL;NSString* callbackId = message[@"callbackId"];if(callbackId) {responseCallback = ^(id responseData) {if(responseData == nil) {responseData = [NSNullnull];}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 = [NSNullnull];}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 messageHandlervar responseCallbackif(message.responseId) {responseCallback = responseCallbacks[message.responseId]if(!responseCallback) {return; }responseCallback(message.responseData)delete responseCallbacks[message.responseId]}else{if(message.callbackId) {var callbackResponseId = message.callbackIdresponseCallback = function(responseData) {_doSend({ responseId:callbackResponseId, responseData:responseData })}}var handler = WebViewJavascriptBridge._messageHandlerif(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,動態讀取調用,從而完成交互。
