iOS源码补完计划-WebViewJavascriptBridge实现原理

提及其原理、所有用过它的童鞋都会说他在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就会直接被执行、完成回调、不会继续向下了。

最后

本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果不吝赐教小弟更加感谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,706评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,002评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,462评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,375评论 0 216
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,763评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,849评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,033评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,768评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,490评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,734评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,204评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,566评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,227评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,137评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,934评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,926评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,774评论 2 274

推荐阅读更多精彩内容