iOS源码补完计划-WebViewJavascriptBridg

作者: kirito_song | 来源:发表于2017-12-19 17:50 被阅读148次

    提及其原理、所有用过它的童鞋都会说他在js和Native(原生)之间搭建了一个桥梁。通过这个桥、使他们相互通信。但具体怎么通信呢?这个桥如何工作?十有八九说却不清。


    JSBridge的逻辑简而言之如下

    • 我这人比较喜欢先贴结论、方便伸手党(像我这种)。

    oc和js相互调用的逻辑都是如此

    (请忽略脑图的指向、能连上就行。由左右向中间)


    JS调用Native
    • 调用方时会生成一个id。
    • 将调用的callback与id(callbackId)绑定备用。
    • 再将方法名与id(callbackId)发给注册方。
    • 注册方通过方法名、找出对应的响应方法(handler)。
    • handler执行完毕后通过id(responseId)找出调用方对应的callback返回。
    • responseId代表被调用方发起、callbackId代表调用方发起、值都相同。
    注意有一点不同
    • js是通过重定向通知oc处理逻辑。参数先存在js中、然后通过oc调用js中_fetchQueue方法被oc获取。
    • oc是通过直接调用_handleMessageFromObjC并且传递了参数通知js处理逻辑。

    正文

    • WebViewJavascriptBridge的原理本质上也是协议拦截。
    • 这个库、具体的用法我就不写了、反正写也是copy别的教学帖子。
      而且、在JSCore以及WKWebView已经极其成熟的当下。WebViewJavascriptBridge用到的地方并不是那么多。
    • 我比较关心的是他如何以注册以及调用这种写法来实现的协议拦截。
      所以我也并不是每行源码都贴出来、只是贴一些关键的功能性代码
    • 其实我以前没用过JSBridge、15年入行的时候就已经是JSCore普及的时代了。
    • 从零开始一行一行读、有兴趣不妨一起。
    • 随手下了一个最新的、2017-12-19:当前版本号6.0.2。

    先看JS调用Native

    • Native中注册:

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

    没什么问题、方法名、js传进来的ballback(参数、回调block)
    继续看.

    #import "WebViewJavascriptBridge.h"
    - (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
        _base.messageHandlers[handlerName] = [handler copy];
    }
    
    • _base:WebViewJavascriptBridge所持有的WebViewJavascriptBridgeBase(简称base)对象。
    • messageHandlers:字典。存储了注册的方法名、ballback。

    然后、线索断了。也就是说、ios这边主动做的事情、已经没了。

    就是在注册的时候将方法名、block。存储起来备用。

    既然是备用、搜索这个函数messageHandlers、我们可以发现。

    • 蓝色部分:WebView已经WKWebview的注册事件、就是上面我们说的那样。
    • 绿色部分:看写法就知道是js文件。内部确实也是js端的注册方法。
    • 红色部分:没错红色部分就是刚才我们使用的这个字典、具体的用处了。

    那继续看红色部分:

    - (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:字符串。本身的格式应该大概是
     "[{"handlerName":"getUserId","data":null,"callbackId":"cb_2_1513740848071"}]"
    

    是个字符串形的json、每部含有三个参数。除了callbackId、我们应该都很好理解。

    • message[@"data"]: 我们注册时候的参数。
    • responseCallback:显而易见是我们注册时候的回调函数。

    那先来看看回调怎么传递给js的吧

     //也是在该方法中、生成了这个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
     };
    

    在我们触发回调的时候、我们responseCallback(@{@"userId": @"123456"});
    其中@{@"userId": @"123456"}。就是这个responseData。
    通过与callbackId关联成一个json。调用_queueMessage方法处理。

    注意。

    这里callbackId已经更名为responseId。
    然后会再走入此大方法一次。进入

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

    然后直接回调给js。这次的回调、才是真正的返还给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:@"\\\\"];
         ******对json字符串进行一系列格式化处理*****
         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函数

    • 至于_handleMessageFromObjC的实现
      就是属于WebViewJavascriptBridge_js文件中的范畴了。一会从js端切入的时候再去看。

    所以说这段代码、就是oc返回给js的回调函数无误。

    再回过头来看看-(void)flushMessageQueue:(NSString *)messageQueueString;方法是如何被调用的

    再次搜索、很明显了、是拦截协议并且判断复合要求之后直接调用的。没什么太绕的东西。


    简单的标注了一下

     - (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发起的url
                 if ([_base isBridgeLoadedURL:url]) {
                     //注入js(WebViewJavascriptBridge_js)
                     [_base injectJavascriptFile];
                 } else if ([_base isQueueMessageURL:url]) {
                     //js主动调启oc
                     NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
                     //去调用刚才分析的那个方法--(通过注册的方法名、调用对应的block)
                     [_base flushMessageQueue:messageQueueString];
                 } else {
                     //控制台报错
                     [_base logUnkownMessage:url];
                 }
                 //拦截
                 return NO;
             } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
                 //正常回调给webView的VC
                 return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
             } else {
                 return YES;
             }
     }
    

    至此、OC注册Handler时所做的事、结束。

    • OC将方法名、block(参数、回调)储存到字典。
    • OC接收到js调用的url、将block取出。传入data/callback并调用该block。
    • OC在方法处理完毕时。通过js传入的callbackId、以及我们的返回值作为参数、调用bridgejs文件中的_handleMessageFromObjC方法。将返回值callback给js中的指定ballback。

    • 需要注意一点的是、JSBridge在发起请求的时候、并不是将参数、callbackId等直接作为url发送出来。而是直接请求https://wvjb_queue_message/(这一点应该算是其中蛮出彩的地方了。很多人也只是知道其使用的是协议拦截)
    • 参数通过bridgejs生成、并且获取。具体这一步如何实现、下面分析js中调用Native的时候再来看(因为现在我也没呢~)。

    • js中调用Native注册的方法:

     //app.html
     bridge.callHandler('getUserId','参数不需要的话可以省略不谢',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);
     }
    

    进行了一些参数处理(js中很多都会根据传入参数数量的不同、内部进行进一步处理)、处理结束直接丢给_doSend函数

    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

    • 就是说js的callback函数在这里会被保存起来。以callbackId为键保存在responseCallbacks这个字典中、将来可以根据callbackId获取、完成回调。
    • callbackId也作为新的参数、添加进了message字典中。

    ok、线索又断了。剩下一个sendMessageQueue以及messagingIframe

    • messagingIframe:

    这个应该比较容易理解。iframe是一个内嵌的网页标签。你既然修改了对应的src(链接)、webView自然会收到一个重定向的请求。

    • sendMessageQueue

    既然修改了iframe的src、让webVIew拦截了协议。sendMessageQueue自然就是为了提供参数而存在的了。

    具体、我们来找找看(搜索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主动调启oc
           NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
           //去调用刚才分析的那个方法--(通过注册的方法名、调用对应的block)
           [_base flushMessageQueue:messageQueueString];
           ***省略***
     }
    
    • _fetchQueue负责提供刚才封装的Message(含有callbackID那个)
    • webViewJavascriptFetchQueyCommand负责在oc中注入js。调用_fetchQueue
    • webView重定向时、调用webViewJavascriptFetchQueyCommand获取参数、并且传递给flushMessageQueue去执行oc中注册方法的block。

    这不是完事了么...

    对啊、这就完事了。注册-调用-回调、一个闭环。具体可以翻回去再看一遍、会恍然大悟。决定画个图。图已经放在最上面了

    OC调用JS

    先看js文件吧、还是想先从注册看起。
    既然我们是iOS开发、js这边就从配置环境开始看代码吧。毕竟很多人还是会很好奇js中的bridge实例从哪来的。

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

    这段呢、是我从网上copy来的。基本所有教学帖子都这么用。
    通过调用setupWebViewJavascriptBridge方法、并传入一个callback函数、来获取bridge对象。

    • 啥是callback?

    其实就是我们block或者闭包、block本质上也就是个代码块而已。只不过js不爱整那么多花花事罢了。
    因为这个bridge对象是在加载完我们iOS本地的bridge_js文件之后才会生成。生成完丢进callBack还给你。

    • 何时加载的bridge_js文件?

    之前我们分析代码的时候已经提到了、iframe修改src会触发webView的代理。方法中第四行的WVJBIframe对象、就是触发加载bridge_js(wvjbscheme://__ BRIDGE_LOADED __)的iframe。

    • 何时返回的bridge对象?

    方法中的1-3行。可能看着有点别扭、我们可以调整一下顺序。

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

    WebViewJavascriptBridge对象是在bridge_js内部被定义以及实现的。

    就是说:

    1、如果有WebViewJavascriptBridge直接返回。
    2、否则每次调用时将callback放入数组。等生成了bridge、再遍历返回。

    初始化就到这、继续看js中注册方法的代码。

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

    感觉和oc注册方法的代码一样(其实注册和调用、两端的方法样式都是相同的)。
    接着看内部、其实这边的实现逻辑。也和oc一样。

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

    注册字典@{方法名:handler函数};

    搜索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);
         }
    
     }
    
    • 根据message.handlerName取出对应的handler、然后把responseCallback丢进去执行。
    • responseCallback哪来的?和OC中的实现一样
      将callbackId、responseData一同返还给oc的回调block。

    继续往回找

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

    -_handleMessageFromObjC:
    这就很眼熟了。之前我们看到这里、然后说留到js这边分析。
    现在想想他做了什么?

    拿到OC发来的messageJSON。里面有responseId/handlerName以及responseData。然后通过responseId将js中对应的callback调起/执行指定已经注册函数。

    然后、最后两个方法。

     - (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发起调用、sendData发送数据。和js调用oc的时候简直一模一样。

    这不是又完事了么...

    嗯、本来以为调用的方式不一样。不过现在看来和js调用oc的方式基本相同。图也就不画了、直接去最上面看就行了。

    最后、如果你也读了源码。肯定对responseId的存在表示疑问。

    • responseId以及callbackId互斥。注册方发起指向调用方、后者表示调用方发起指向被调用方。
    • 相应的。handlerName也只是在存在callbackId的时候才存在、并且执行handler。因为如果存在responseId、那个responseCallback就会直接被执行、完成回调、不会继续向下了。

    相关文章

      网友评论

        本文标题:iOS源码补完计划-WebViewJavascriptBridg

        本文链接:https://www.haomeiwen.com/subject/emjiwxtx.html