WebViewJavascriptBridge 原理分析


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文件中點擊一個按鈕發起請求的代碼:

 

 
1
2
3
4
5
6
7
8
9
10
var 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)
             })
         }

估計大家大體都能看懂,唯獨有疑問的地方是:

 

 

?
1
2
3
4
bridge.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里面我們找找這個方法:

 

?
1
2
3
function callHandler(handlerName, data, responseCallback) {
         _doSend({ handlerName:handlerName, data:data }, responseCallback)
     }

這里又多了一個方法叫_doSend連個參數 第1個是字典key-value定義,第二個是一個方法的指針(看看上面的方法你就知道了),那我們必須在同一個文件里面看看能不能找到這個_doSend方法:

 

 

?
1
2
3
4
5
6
7
8
9
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
     }

找到了。

 

逐行分析一下,變量callbackId是個字符串,responseCallBacks[] 一看就知道是個字典 ,這個字典把回掉(我們猜測)的方法responseCallback給保存起來,這Key(也就是callbackId)應該是唯一的,通過計數和時間應該知道這個字符串應該是唯一的,message也是一個字典,這是給message添加了一個新的key-value。干嘛呢?我也不知道,我們來看看sendMessageQueue是什么,大家一個push就知道應該是個數組。他吧一個字典放到一個消息隊列中(數組隊列),讓后產生一個src(url scheme)。

有兩個變量我們看看:

 

?
1
2
var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme'
var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__'

 

干嘛用,肯定是給webview 的 delegate判斷用的,你感覺呢?(肯定是)

 

下面是在文件:WebViewJavascriptBridge.m

好了到了這里大家猜猜這個要干嘛?肯定是要發url讓web截取對吧?那還用問啊,肯定是啊,已經說過了js能不能調用native的funtion函數?不能。我們來看看這個messagingIframe是:

 

?
1
2
3
4
5
6
function _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下面:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (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:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
- ( 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中的方法:

 

 
1
2
3
4
5
function _fetchQueue() {
     var messageQueueString = JSON.stringify(sendMessageQueue)
     sendMessageQueue = []
     return messageQueueString
}

謝天謝地這個方法代碼不多,這個消息很眼熟,SendMessageQueue,剛才我們說什么來?他是一個字典,那里面有哪些東西,我么來看看

 

handlerName:handlerName,

data:data,

callbackId:callbackId

這個消息字典此時被取出來准備做什么,這里提示下我們已經走到webview 的delegate里面了,所以拿到這些信息肯定是調用native的method對吧?肯定是的。接着往下走,接着會把json字符串轉成數組,然后進行判斷,

?
1
NSString* responseId = message[@ "responseId" ];
有沒有responseid,你說又沒,肯定沒有啊(你不行看看上面),所以就這這里了

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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);
這部分是重點,到底他是怎么要調用本地function的,callbackId大家熟悉吧,判斷是否為空,不為空給他指定一個block,這個不說了,block指定,此時不調用(手動調用才會執行),這個剛才說了用來處理native的function處理的result用於把處理后的值返回給js的,接着往下去,看到handler這個方法會從message找到handlerName,這里我們看一下多了一個_messageHandlers字典,從這個字典獲取一個block(WVJBHandler是一個block),直接執行了。那我們看看_messageHandlers是怎么被添加block的:

 

 

 
1
2
3
- ( void )registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
     _messageHandlers[handlerName] = [handler copy];
}

那又是誰調用了這個方法:

 

找到了(在文件 ExampleAppViewController.m的viewdidload中),這里有方法testObjecCallback

 

 
1
2
3
4
[_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中調用而調用,有點繞,自己可以多想想。

 

我們接着來看看:

 

 
1
2
3
4
5
6
7
8
responseCallback = ^(id responseData) {
                     if (responseData == nil) {
                         responseData = [NSNull null ];
                     }
                     
                     WVJBMessage* msg = @{ @ "responseId" :callbackId, @ "responseData" :responseData };
                     [self _queueMessage:msg];
                 };

這個就是你繞的地方,他是后被定義的,所以一開不執行,只有在處理數據后回調才會被調用,這里有個方法_queueMessage:

 

 

 
1
2
3
4
5
6
7
- ( void )_queueMessage:(WVJBMessage*)message {
     if (_startupMessageQueue) {
         [_startupMessageQueue addObject:message];
     } else {
         [self _dispatchMessage:message];
     }
}

這里面還有個方法:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- ( 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文件中看到

 

?
1
2
3
4
5
6
7
function _handleMessageFromObjC(messageJSON) {
     if (receiveMessageQueue) {
         receiveMessageQueue.push(messageJSON)
     } else { //肯定走這個  為什么呢?
         _dispatchMessageFromObjC(messageJSON)
     }
}

再來看看:

 

 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function _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意思一樣,只要值沒變就行,所以就會執行:

 

 
1
2
3
bridge.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過程一樣的處理方式。

大體來看一下,先看一個按鈕的單擊事件:

 

 
1
2
3
4
5
6
- ( void )callHandler:(id)sender {
     id data = @{ @ "greetingFromObjC" : @ "Hi there, JS!" };
     [_bridge callHandler:@ "testJavascriptHandler" data:data responseCallback:^(id response) {
         NSLog(@ "testJavascriptHandler responded: %@" , response);
     }];
}

 

看看callHandler:

 

 
1
2
3
- ( void )callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
     [self _sendData:data responseCallback:responseCallback handlerName:handlerName];
}

看看_sendData:

 

 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- ( 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也有個:

 

 

 
1
2
3
4
5
6
NSString* 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,動態讀取調用,從而完成交互。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM