iOS源碼補(bǔ)完計(jì)劃-WebViewJavascriptBridge實(shí)現(xiàn)原理

提及其原理、所有用過它的童鞋都會(huì)說(shuō)他在js和Native(原生)之間搭建了一個(gè)橋梁。通過這個(gè)橋、使他們相互通信。但具體怎么通信呢?這個(gè)橋如何工作?十有八九說(shuō)卻不清。


JSBridge的邏輯簡(jiǎn)而言之如下

  • 我這人比較喜歡先貼結(jié)論、方便伸手黨(像我這種)。

oc和js相互調(diào)用的邏輯都是如此

(請(qǐng)忽略腦圖的指向、能連上就行。由左右向中間)


JS調(diào)用Native
  • 調(diào)用方時(shí)會(huì)生成一個(gè)id。
  • 將調(diào)用的callback與id(callbackId)綁定備用。
  • 再將方法名與id(callbackId)發(fā)給注冊(cè)方。
  • 注冊(cè)方通過方法名、找出對(duì)應(yīng)的響應(yīng)方法(handler)。
  • handler執(zhí)行完畢后通過id(responseId)找出調(diào)用方對(duì)應(yīng)的callback返回。
  • responseId代表被調(diào)用方發(fā)起、callbackId代表調(diào)用方發(fā)起、值都相同。
注意有一點(diǎn)不同
  • js是通過重定向通知oc處理邏輯。參數(shù)先存在js中、然后通過oc調(diào)用js中_fetchQueue方法被oc獲取。
  • oc是通過直接調(diào)用_handleMessageFromObjC并且傳遞了參數(shù)通知js處理邏輯。

正文

  • WebViewJavascriptBridge的原理本質(zhì)上也是協(xié)議攔截。
  • 這個(gè)庫(kù)、具體的用法我就不寫了、反正寫也是copy別的教學(xué)帖子。
    而且、在JSCore以及WKWebView已經(jīng)極其成熟的當(dāng)下。WebViewJavascriptBridge用到的地方并不是那么多。
  • 我比較關(guān)心的是他如何以注冊(cè)以及調(diào)用這種寫法來(lái)實(shí)現(xiàn)的協(xié)議攔截。
    所以我也并不是每行源碼都貼出來(lái)、只是貼一些關(guān)鍵的功能性代碼
  • 其實(shí)我以前沒用過JSBridge、15年入行的時(shí)候就已經(jīng)是JSCore普及的時(shí)代了。
  • 從零開始一行一行讀、有興趣不妨一起。
  • 隨手下了一個(gè)最新的、2017-12-19:當(dāng)前版本號(hào)6.0.2。

先看JS調(diào)用Native

  • Native中注冊(cè):

[self.bridge registerHandler:@"getUserId" handler:^(id data, WVJBResponseCallback responseCallback) {
   if (responseCallback) {
         // 反饋給JS
         responseCallback(@{@"userId": @"123456"});
    }
}];

沒什么問題、方法名、js傳進(jìn)來(lái)的ballback(參數(shù)、回調(diào)block)
繼續(xù)看.

#import "WebViewJavascriptBridge.h"
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}
  • _base:WebViewJavascriptBridge所持有的WebViewJavascriptBridgeBase(簡(jiǎn)稱base)對(duì)象。
  • messageHandlers:字典。存儲(chǔ)了注冊(cè)的方法名、ballback。

然后、線索斷了。也就是說(shuō)、ios這邊主動(dòng)做的事情、已經(jīng)沒了。

就是在注冊(cè)的時(shí)候?qū)⒎椒?、block。存儲(chǔ)起來(lái)備用。

既然是備用、搜索這個(gè)函數(shù)messageHandlers、我們可以發(fā)現(xiàn)。

  • 藍(lán)色部分:WebView已經(jīng)WKWebview的注冊(cè)事件、就是上面我們說(shuō)的那樣。
  • 綠色部分:看寫法就知道是js文件。內(nèi)部確實(shí)也是js端的注冊(cè)方法。
  • 紅色部分:沒錯(cuò)紅色部分就是剛才我們使用的這個(gè)字典、具體的用處了。

那繼續(xù)看紅色部分:

- (void)flushMessageQueue:(NSString *)messageQueueString;
 #import "WebViewJavascriptBridgeBase.h"
 - (void)flushMessageQueue:(NSString *)messageQueueString{
   //省略掉其他代碼之后
   ......
   WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
             
   if (!handler) {
      NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
      continue;
   }
         
   handler(message[@"data"], responseCallback);
 }
  • messageQueueString:字符串。本身的格式應(yīng)該大概是
 "[{"handlerName":"getUserId","data":null,"callbackId":"cb_2_1513740848071"}]"

是個(gè)字符串形的json、每部含有三個(gè)參數(shù)。除了callbackId、我們應(yīng)該都很好理解。

  • message[@"data"]: 我們注冊(cè)時(shí)候的參數(shù)。
  • responseCallback:顯而易見是我們注冊(cè)時(shí)候的回調(diào)函數(shù)。

那先來(lái)看看回調(diào)怎么傳遞給js的吧

 //也是在該方法中、生成了這個(gè)responseCallback
 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
 };

在我們觸發(fā)回調(diào)的時(shí)候、我們r(jià)esponseCallback(@{@"userId": @"123456"});
其中@{@"userId": @"123456"}。就是這個(gè)responseData。
通過與callbackId關(guān)聯(lián)成一個(gè)json。調(diào)用_queueMessage方法處理。

注意。

這里callbackId已經(jīng)更名為responseId。
然后會(huì)再走入此大方法一次。進(jìn)入

 if (responseId) {
       WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
       responseCallback(message[@"responseData"]);
       [self.responseCallbacks removeObjectForKey:responseId];
   } 

然后直接回調(diào)給js。這次的回調(diào)、才是真正的返還給js。

 - (void)_queueMessage:(WVJBMessage*)message {
     if (self.startupMessageQueue) {
         [self.startupMessageQueue addObject:message];
     } else {
         [self _dispatchMessage:message];
     }
 }
 
 - (void)_dispatchMessage:(WVJBMessage*)message {
     NSString *messageJSON = [self _serializeMessage:message >      pretty:NO];
     [self _log:@"SEND" json:messageJSON];
     messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
     ******對(duì)json字符串進(jìn)行一系列格式化處理*****
     messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
     
     NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
     if ([[NSThread currentThread] isMainThread]) {
         [self _evaluateJavascript:javascriptCommand];
 
     } else {
         dispatch_sync(dispatch_get_main_queue(), ^{
             [self _evaluateJavascript:javascriptCommand];
         });
     }
 }
  • message:

     {"responseId":"cb_3_1513741962583","responseData":{"userId":"123456"}};
    
  • javascriptCommand:

     WebViewJavascriptBridge._handleMessageFromObjC('{\"responseId\":\"cb_3_1513741962583\",\"responseData\":{\"userId\":\"123456\"}}');
    
  • _evaluateJavascript:方法
    底層是讓webview去注入這段js函數(shù)

  • 至于_handleMessageFromObjC的實(shí)現(xiàn)
    就是屬于WebViewJavascriptBridge_js文件中的范疇了。一會(huì)從js端切入的時(shí)候再去看。

所以說(shuō)這段代碼、就是oc返回給js的回調(diào)函數(shù)無(wú)誤。

再回過頭來(lái)看看-(void)flushMessageQueue:(NSString *)messageQueueString;方法是如何被調(diào)用的

再次搜索、很明顯了、是攔截協(xié)議并且判斷復(fù)合要求之后直接調(diào)用的。沒什么太繞的東西。


簡(jiǎn)單的標(biāo)注了一下

 - (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 ([_base isWebViewJavascriptBridgeURL:url]) {
             //js通過Bridge發(fā)起的url
             if ([_base isBridgeLoadedURL:url]) {
                 //注入js(WebViewJavascriptBridge_js)
                 [_base injectJavascriptFile];
             } else if ([_base isQueueMessageURL:url]) {
                 //js主動(dòng)調(diào)啟oc
                 NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
                 //去調(diào)用剛才分析的那個(gè)方法--(通過注冊(cè)的方法名、調(diào)用對(duì)應(yīng)的block)
                 [_base flushMessageQueue:messageQueueString];
             } else {
                 //控制臺(tái)報(bào)錯(cuò)
                 [_base logUnkownMessage:url];
             }
             //攔截
             return NO;
         } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
             //正?;卣{(diào)給webView的VC
             return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
         } else {
             return YES;
         }
 }

至此、OC注冊(cè)Handler時(shí)所做的事、結(jié)束。

  • OC將方法名、block(參數(shù)、回調(diào))儲(chǔ)存到字典。
  • OC接收到j(luò)s調(diào)用的url、將block取出。傳入data/callback并調(diào)用該block。
  • OC在方法處理完畢時(shí)。通過js傳入的callbackId、以及我們的返回值作為參數(shù)、調(diào)用bridgejs文件中的_handleMessageFromObjC方法。將返回值callback給js中的指定ballback。

  • 需要注意一點(diǎn)的是、JSBridge在發(fā)起請(qǐng)求的時(shí)候、并不是將參數(shù)、callbackId等直接作為url發(fā)送出來(lái)。而是直接請(qǐng)求https://wvjb_queue_message/(這一點(diǎn)應(yīng)該算是其中蠻出彩的地方了。很多人也只是知道其使用的是協(xié)議攔截)
  • 參數(shù)通過bridgejs生成、并且獲取。具體這一步如何實(shí)現(xiàn)、下面分析js中調(diào)用Native的時(shí)候再來(lái)看(因?yàn)楝F(xiàn)在我也沒呢~)。

  • js中調(diào)用Native注冊(cè)的方法:

 //app.html
 bridge.callHandler('getUserId','參數(shù)不需要的話可以省略不謝',function(response){
   log(response.userId)
 })

 //WebViewJavascriptBridge_JS
 function callHandler(handlerName, data, responseCallback) {
      if (arguments.length == 2 && typeof data == 'function') {
          responseCallback = data;
          data = null;
      }
      _doSend({ handlerName:handlerName, data:data }, responseCallback);
 }

進(jìn)行了一些參數(shù)處理(js中很多都會(huì)根據(jù)傳入?yún)?shù)數(shù)量的不同、內(nèi)部進(jìn)行進(jìn)一步處理)、處理結(jié)束直接丟給_doSend函數(shù)

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;
  }

這里我們看到了一個(gè)很熟悉的參名字callbackId

  • 就是說(shuō)js的callback函數(shù)在這里會(huì)被保存起來(lái)。以callbackId為鍵保存在responseCallbacks這個(gè)字典中、將來(lái)可以根據(jù)callbackId獲取、完成回調(diào)。
  • callbackId也作為新的參數(shù)、添加進(jìn)了message字典中。

ok、線索又?jǐn)嗔?。剩下一個(gè)sendMessageQueue以及messagingIframe

  • messagingIframe:

這個(gè)應(yīng)該比較容易理解。iframe是一個(gè)內(nèi)嵌的網(wǎng)頁(yè)標(biāo)簽。你既然修改了對(duì)應(yīng)的src(鏈接)、webView自然會(huì)收到一個(gè)重定向的請(qǐng)求。

  • sendMessageQueue

既然修改了iframe的src、讓webVIew攔截了協(xié)議。sendMessageQueue自然就是為了提供參數(shù)而存在的了。

具體、我們來(lái)找找看(搜索sendMessageQueue)。

//WebViewJavascriptBridge_JS
function _fetchQueue() {
      var messageQueueString = JSON.stringify(sendMessageQueue);
      sendMessageQueue = [];
      return messageQueueString;
}
//#import "WebViewJavascriptBridgeBase.h"
- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}

 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
       ***省略***
       //js主動(dòng)調(diào)啟oc
       NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
       //去調(diào)用剛才分析的那個(gè)方法--(通過注冊(cè)的方法名、調(diào)用對(duì)應(yīng)的block)
       [_base flushMessageQueue:messageQueueString];
       ***省略***
 }
  • _fetchQueue負(fù)責(zé)提供剛才封裝的Message(含有callbackID那個(gè))
  • webViewJavascriptFetchQueyCommand負(fù)責(zé)在oc中注入js。調(diào)用_fetchQueue
  • webView重定向時(shí)、調(diào)用webViewJavascriptFetchQueyCommand獲取參數(shù)、并且傳遞給flushMessageQueue去執(zhí)行oc中注冊(cè)方法的block。

這不是完事了么...

對(duì)啊、這就完事了。注冊(cè)-調(diào)用-回調(diào)、一個(gè)閉環(huán)。具體可以翻回去再看一遍、會(huì)恍然大悟。決定畫個(gè)圖。圖已經(jīng)放在最上面了

OC調(diào)用JS

先看js文件吧、還是想先從注冊(cè)看起。
既然我們是iOS開發(fā)、js這邊就從配置環(huán)境開始看代碼吧。畢竟很多人還是會(huì)很好奇js中的bridge實(shí)例從哪來(lái)的。

  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 = 'wvjbscheme://__BRIDGE_LOADED__';
          document.documentElement.appendChild(WVJBIframe);
          setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
  }
setupWebViewJavascriptBridge(function(bridge) {
     <!--   操作bridge   -->     
}

這段呢、是我從網(wǎng)上copy來(lái)的?;舅薪虒W(xué)帖子都這么用。
通過調(diào)用setupWebViewJavascriptBridge方法、并傳入一個(gè)callback函數(shù)、來(lái)獲取bridge對(duì)象。

  • 啥是callback?

其實(shí)就是我們block或者閉包、block本質(zhì)上也就是個(gè)代碼塊而已。只不過js不愛整那么多花花事罷了。
因?yàn)檫@個(gè)bridge對(duì)象是在加載完我們iOS本地的bridge_js文件之后才會(huì)生成。生成完丟進(jìn)callBack還給你。

  • 何時(shí)加載的bridge_js文件?

之前我們分析代碼的時(shí)候已經(jīng)提到了、iframe修改src會(huì)觸發(fā)webView的代理。方法中第四行的WVJBIframe對(duì)象、就是觸發(fā)加載bridge_js(wvjbscheme://__ BRIDGE_LOADED __)的iframe。

  • 何時(shí)返回的bridge對(duì)象?

方法中的1-3行??赡芸粗悬c(diǎn)別扭、我們可以調(diào)整一下順序。

  if (window.WebViewJavascriptBridge) {
         callback(WebViewJavascriptBridge); 
         return ;
  }
  if (window.WVJBCallbacks) {         
        window.WVJBCallbacks.push(callback);
        return;
 }
 window.WVJBCallbacks = [callback];

WebViewJavascriptBridge對(duì)象是在bridge_js內(nèi)部被定義以及實(shí)現(xiàn)的。

就是說(shuō):

1、如果有WebViewJavascriptBridge直接返回。
2、否則每次調(diào)用時(shí)將callback放入數(shù)組。等生成了bridge、再遍歷返回。

初始化就到這、繼續(xù)看js中注冊(cè)方法的代碼。

 bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
     var responseData = { 'Javascript Says':'Right back atcha!' };
     if(responseCallback) {
         responseCallback(responseData);
     };
 });

感覺和oc注冊(cè)方法的代碼一樣(其實(shí)注冊(cè)和調(diào)用、兩端的方法樣式都是相同的)。
接著看內(nèi)部、其實(shí)這邊的實(shí)現(xiàn)邏輯。也和oc一樣。

 function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
  }

注冊(cè)字典@{方法名:handler函數(shù)};

搜索messageHandlers

 function _dispatchMessageFromObjC(messageJSON) {
     var handler = messageHandlers[message.handlerName];
     if (!handler) {
          console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
     } else {
          handler(message.data, responseCallback);
     }

 }
  • 根據(jù)message.handlerName取出對(duì)應(yīng)的handler、然后把responseCallback丟進(jìn)去執(zhí)行。
  • responseCallback哪來(lái)的?和OC中的實(shí)現(xiàn)一樣
    將callbackId、responseData一同返還給oc的回調(diào)block。

繼續(xù)往回找

 function _handleMessageFromObjC(messageJSON) {
     _dispatchMessageFromObjC(messageJSON);
}

-_handleMessageFromObjC:
這就很眼熟了。之前我們看到這里、然后說(shuō)留到j(luò)s這邊分析。
現(xiàn)在想想他做了什么?

拿到OC發(fā)來(lái)的messageJSON。里面有responseId/handlerName以及responseData。然后通過responseId將js中對(duì)應(yīng)的callback調(diào)起/執(zhí)行指定已經(jīng)注冊(cè)函數(shù)。

然后、最后兩個(gè)方法。

 - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
     //封裝message@{callbackId/handlerName/data}
     [self _queueMessage:message];
 }

 - (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
     [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
 }

callHandler發(fā)起調(diào)用、sendData發(fā)送數(shù)據(jù)。和js調(diào)用oc的時(shí)候簡(jiǎn)直一模一樣。

這不是又完事了么...

嗯、本來(lái)以為調(diào)用的方式不一樣。不過現(xiàn)在看來(lái)和js調(diào)用oc的方式基本相同。圖也就不畫了、直接去最上面看就行了。

最后、如果你也讀了源碼。肯定對(duì)responseId的存在表示疑問。

  • responseId以及callbackId互斥。注冊(cè)方發(fā)起指向調(diào)用方、后者表示調(diào)用方發(fā)起指向被調(diào)用方。
  • 相應(yīng)的。handlerName也只是在存在callbackId的時(shí)候才存在、并且執(zhí)行handler。因?yàn)槿绻嬖趓esponseId、那個(gè)responseCallback就會(huì)直接被執(zhí)行、完成回調(diào)、不會(huì)繼續(xù)向下了。

最后

本文主要是自己的學(xué)習(xí)與總結(jié)。如果文內(nèi)存在紕漏、萬(wàn)望留言斧正。如果不吝賜教小弟更加感謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容