iOS开发 - WebViewJavascriptBridge分析

强烈推荐:iOS源码补完计划-WebViewJavascriptBridge实现原理

iOS下JS与OC互相调用(六)--WKWebView + WebViewJavascriptBridge

JS调用OC

//将版本信息发送给Html
[_WKwebViewBridge registerHandler:@"GetCurrentVersion" handler:^(id data, WVJBResponseCallback responseCallback) {
    // 获取当前版本号
    NSString *appVersion = @"1.0.0";
    // 反馈给JS
    responseCallback(appVersion);
}];
#import "WebViewJavascriptBridge.h"

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}
  • _base
    @implementation WKWebViewJavascriptBridge {
    WebViewJavascriptBridgeBase *_base;
    }
    WebViewJavascriptBridge所持有的WebViewJavascriptBridgeBase(简称base)对象。
  • messageHandlers
    @property (strong, nonatomic) NSMutableDictionary* messageHandlers;
    字典。存储了注册的方法名、ballback。

就是在注册的时候将方法名、block。存储起来备用。
然后、线索断了。也就是说、ios这边主动做的事情、已经没了。

既然存储起来了,那又在什么地方使用了呢?我们搜索messageHandlers看看

messageHandlers的调用

进一步,我们查看messageHandlers调用的地方 - (void)flushMessageQueue:(NSString *)messageQueueString

- (void)flushMessageQueue:(NSString *)messageQueueString{
    if (messageQueueString == nil || messageQueueString.length == 0) {
           // 省略 .......... 
 
           WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            
            handler(message[@"data"], responseCallback);
        }
    }
}

messageQueueString :是一个字符串,内部数据格式如下:

messageQueueString具体数据
"[{"handlerName":"GetCurrentVersion","data":{},"callbackId":"cb_2_1548142676517"}]"

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

handler(message[@"data"], responseCallback); 运行我们注册的回调函数,会回到我们注册的地方:

注册时候的block代码

运行OC代码,向JS反馈信息调用responseCallback(appVersion),又回到了- (void)flushMessageQueue:(NSString *)messageQueueString

- (void)flushMessageQueue:(NSString *)messageQueueString{
    // 省略 ..............
            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
                };
            }
    // 省略 ..............
}

发现我们传递过来的appVersion 就是 responseData 的值,并且重新整理成一个WVJBMessage(也就是NSDictionary)对象。

- (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];
    // 对json字符串进行一系列格式化处理
    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]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}
message 的值
{
    responseData = "1.1.3";
    responseId = "cb_2_1548206601903";
}
javascriptCommand 的值
WebViewJavascriptBridge._handleMessageFromObjC('{\"responseId\":\"cb_2_1548206601903\",\"responseData\":\"1.1.3\"}');

将获取的数据整理成一个WVJBMessage(也就是NSDictionary)对象后,调用_evaluateJavascript:方法,底层是让webview去注入这段js函数
至于_handleMessageFromObjC的实现,就是属于WebViewJavascriptBridge_js文件中的范畴了。一会从js端切入的时候再去看。

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

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

WKWebView中调用flushMessageQueue
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    // js通过Bridge发起的url
    if ([_base isCorrectProcotocolScheme:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            // 注入js(WebViewJavascriptBridge_js)
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            // js主动调启oc,也就是我们上面分析的步骤
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        // 拦截
        decisionHandler(WKNavigationActionPolicyCancel);
    }
    else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        // 不拦截,正常回调给webView的VC
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}


- (void)WKFlushMessageQueue {
    // webView执行JS `WebViewJavascriptBridge._fetchQueue();`
    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        [_base flushMessageQueue:result];
    }];
}

WKWebView执行JSWebViewJavascriptBridge._fetchQueue();,得到result

"[{"handlerName":"GetCurrentVersion","data":{},"callbackId":"cb_2_1548208471860"}]"

但是又如何触发 webView:decidePolicyForNavigationAction:decisionHandler: 这个代理方法呢?

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;
}
  • 就是说js的callback函数在这里会被保存起来。以callbackId为键保存在responseCallbacks这个字典中、将来可以根据callbackId获取、完成回调。

  • callbackId也作为新的参数、添加进了message字典中。

  • 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();";
}


- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
/*** 省略 ***/
    // js主动调启oc
    [self WKFlushMessageQueue];
/*** 省略 ***/
}


- (void)WKFlushMessageQueue {
    // webView执行JS `WebViewJavascriptBridge._fetchQueue();`
    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        [_base flushMessageQueue:result];
    }];
}

这里修改src = wvjbscheme://__WVJB_QUEUE_MESSAGE__,也就是我们后面拦截协议时,拦截到的url,那么参数也就是 webview执行JSWebViewJavascriptBridge._fetchQueue(); 从而获取sendMessageQueue.push(message)传递出来的值。

总结

1、OC端注册,将 方法名 + 回调 存储到 messageHandlers 中;
2、JS发出请求,修改iframe的src,进行重定向,webview触发代理回调webView: decidePolicyForNavigationAction: decisionHandler:进行协议拦截;
3、OC拦截到URL,注意这里的URL并不是将参数、callbackId等直接作为url发送出来,而是wvjbscheme://__WVJB_QUEUE_MESSAGE__,那么参数又是怎么来的呢?
4、wkwebview执行JS代码WebViewJavascriptBridge._fetchQueue();从而获得了具体的参数:"[{"handlerName":"GetCurrentVersion","data":{},"callbackId":"cb_2_1548208471860"}]"
5、拿到这些参数后,将存储在 messageHandlers 中的block执行,也就是执行注册时的回掉了(这里可以执行OC相关代码,我们这里是获取版本号)。
6、当拿到版本号后,需要反馈给JS,将拿到的数据整理成新的数据 @{ @"responseId":callbackId, @"responseData":responseData }; ,调用bridgejs文件中的 _handleMessageFromObjC 方法。将返回值callback给js中的指定callback。




OC调用JS

先看js文件,还是想先从注册看起。

js中的bridge实例初始化。

var _setupWebViewJavascriptBridge = function(callback){   //ios桥函数
    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)
};

js中注册方法的代码

bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
     var responseData = { 'Javascript Says':'Right back atcha!' };
     if(responseCallback) {
         responseCallback(responseData);
     };
 });
// WebViewJavascriptBridge_JS.h
function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}

OC又是如何调用的呢?

[_WKwebViewBridge callHandler:@"testJavascriptHandler" data:@"some message"];

- (void)_dispatchMessage:(WVJBMessage*)message {
/*** 省略 ***/
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];
    } 
/*** 省略 ***/
}

_handleMessageFromObjC又做了啥?

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

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

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

推荐阅读更多精彩内容