WebViewJavaScriptBridge源码剖析

亲,我的简书已不再维护和更新了,所有文章都迁移到了我的个人博客:https://mikefighting.github.io/,欢迎交流。

WebViewJavaScriptBridge是IOS中JS和OC交互的常用框架,它利用block的形式处理回调(相关Demo已上传),支持以下两种调用:

基本用法

它的两种使用场景如下:

WebViewJavaScriptBridge使用场景

1. OC端的方法如下

Method Frome OC

Method 1 是注册一个OC的方法--testObjcCallback,handler是JS掉用的内容,responseCallback是将OC处理返回给JS的回调(对应的是上述第2种调用);
Method 2 是调用JS的方法的testJavascriptHandler方法,@{ @"foo":@"before ready" }是需要传递的参数,responseCallback是将JS处理结果返回给OC的回调(对应的是上述的第1种调用)

2. JS端的方法如下

Method Frome JS

Method 1 是JS注册一个方法供OC调用,responseCallback(responseData)是将处理结果返回OC。
Method 2 是在点击了一个按钮之后JS调用OC的方法,{'foo': 'bar'}是给OC的参数,response是OC处理后返回给JS的数据。
注:JS中是可以不写;号的,这和swift一样

JS调用OC,OC将处理结果回调给JS:要想被JS调用,我们首先要注册一个handler,和回调的 block,注册时候以键值对的形式存储这个block,handler,当JS调用OC时调用webView:shouldStartLoadWithRequest:navigationType:这个方法,根据JS传来的数据,找到之前保存的Block并且调用,同时新建一个需要把处理结果回调给JS的Blcok,OC处理完结果之后调用刚才创建的Block利用stringByEvaluatingJavaScriptFromString将处理结果返回给JS。
OC调用JS时与此类似。基于这个流程,我们来看WebViewJavaScriptBridge的实现过程。

原理

接下来我们来分析从页面加载到OC和JS互相调用的整个过程:

一、准备工作

当加载HTML文件的时候调用[webView loadHTMLString:appHtml baseURL:baseURL];,这时会调用:

  - (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]) {
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            [_base flushMessageQueue:messageQueueString];
        } else {
            [_base logUnkownMessage:url];
        }
        return NO;
    } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    } else {
        return YES;
    }
}

在这个方法中判断URL的类型,如果是WebViewJavascriptBridgeURL那么就会判断是BridgeLoadedURLQueueMessageURL还是未知的URL,在首次调用时是返回YES的,然后的URL就是BridgeLoadedURL,我们在看它的判断条件[self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];Scheme是自己设置的https,那么BridgeLoaded(__bridge_loaded__)是什么呢?我们看ExampleApp.html文件,发现它的script标签中有这么一段代码:

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 = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

setupWebViewJavascriptBridge(function(bridge) {
    var uniqueId = 1
    function log(message, data) {
        var log = document.getElementById('log')
        var el = document.createElement('div')
        el.className = 'logLine'
        el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
        if (log.children.length) { log.insertBefore(el, log.children[0]) }
        else { log.appendChild(el) }
    }

在这里我们发现了https://__bridge_loaded__这个iframe的src,并且在接下来调用setupWebViewJavascriptBridge时这个src会当做一个请求,这时会调用shouldStartLoadWithRequest这个方法。此时就满足了isBridgeLoadedURL这个请求。这时就会调用

[_base injectJavascriptFile]

注入一个JS文件,这个JS文件的主要内容是(篇幅问题,有删减):

window.WebViewJavascriptBridge = {
    registerHandler: registerHandler,
    callHandler: callHandler,
    disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
    _fetchQueue: _fetchQueue,
    _handleMessageFromObjC: _handleMessageFromObjC
};

var messagingIframe;
var sendMessageQueue = [];
var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;

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

function callHandler(handlerName, data, responseCallback) {

    _doSend();
}

function disableJavscriptAlertBoxSafetyTimeout() {
    dispatchMessagesWithTimeoutSafety = false;
}

function _doSend(message, responseCallback) {
    sendMessageQueue.push(message);
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    return messageQueueString;
}

function _dispatchMessageFromObjC(messageJSON) {
    if (dispatchMessagesWithTimeoutSafety) {
        setTimeout(_doDispatchMessageFromObjC);
    } else {
         _doDispatchMessageFromObjC();
    }
    
function _doDispatchMessageFromObjC() {

        }
    }
}

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

messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);

registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);

setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
    var callbacks = window.WVJBCallbacks;
    delete window.WVJBCallbacks;
    for (var i=0; i<callbacks.length; i++) {
        callbacks[i](WebViewJavascriptBridge);
    }
}

下面我们来分析下注入的JavaScript的内容。

  1. 给window对象添加一个属性WebViewJavascriptBridgeJS中可以直接给对象添加属性),这个对象包含以下内容:
  1) registerHandler:注册调用方法
  2)callHandler:调用OC时的方法
  3)disableJavscriptAlertBoxSafetyTimeout:超时时弹框是否展示的标示
  4)_fetchQueue:获取Queue对象的方法
  5)_handleMessageFromObjC:处理OC调用的方法

2.定义了一系列的变量来存储数据
messagingIframe:iframe标签,当我们的WebView加载它的时候,会调用其中的src,src就是调用请求的URL。

  1)sendMessageQueue:message数组
  2)messageHandlers:handler对象 *JS中{}表示对象*
  3)CUSTOM_PROTOCOL_SCHEME:scheme标示
  4)QUEUE_HAS_MESSAGE:有Message标识
  5)responseCallbacks:回调对象
  6)uniqueId:唯一标示ID

进过系列一的剖析,我们明白了使用WebViewJavaScriptBridge前需要做的准备工作,那么接下来,我们一起探讨OCJS相互调用的具体执行过程以及其中的要点。

二、 JS调用OC,然后OC将处理结果返回JS

1. OC首先注册JS将调用的方法

OC调用registerHandler:,这时将其调用信息存储在messageHandlers字典中以handlerName为Key,给JS处理结果的Block为Value(_base.messageHandlers[handlerName] = [handler copy]);

2. 在JS中调用被注册的方法

JS调用

bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
            log('JS got response', response)
        })

来调用上文OC注册的方法,这个brige就是上文注入JS代码时候创建的,我们再它内部做了什么。

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

这里判断了参数类型,如果传入的参数只有两个,并且第二个是function类型,那么就将第二个参数变为callBack,data置空,将handlerName和data转化成一个对象的两个属性并传给_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;
        }

这里的responseCallback是JS先调用OC,在OC给JS回调结果时用的,这里用唯一的标识(callbackId),来将这个responseCallback存储在responseCallbacks中,并且给message添加callbackId 这个属性。这个数值会在下次OC调用JS的时候作为唯一的Key被用到,以便从中找到JS端的方法回调。软后将message放入:sendMessageQueue队列中,然后拼接src。

3. 在回掉方法中拦截相应的方法,然后调用block.

经过方法步骤2,会调用下面的回调方法

    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{}

在这个方法中调用

  NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
  [_base flushMessageQueue:messageQueueString];

首先获取JS中的messageQueue(步骤2中的sendMessageQueue),然后调用flushMessageQueue:方法:

 id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
    if (![message isKindOfClass:[WVJBMessage class]]) {
        NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
        continue;
    }
    [self _log:@"RCVD" json:message];
    
    /////////*********OC先调用了JS,JS再调用了OC*********///////////
    NSString* responseId = message[@"responseId"];
    if (responseId) {
        //调用之前存储的Bolck
        WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
        responseCallback(message[@"responseData"]);
        [self.responseCallbacks removeObjectForKey:responseId];
        
    /////////*********JS先调用OC,OC再调用JS*********///////////
    /// 这里是JS先调用OC的时候存储的是 JS的回调函数
    } else {
        
        // JS先调用的OC,OC再调用JS
        WVJBResponseCallback responseCallback = NULL;
        NSString* callbackId = message[@"callbackId"];
        if (callbackId) {
            responseCallback = ^(id responseData) {
                if (responseData == nil) {
                    responseData = [NSNull null];
                }
                //JS调用OC时候的存储(后续OC调用JS返回计算结果)
                WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                [self _queueMessage:msg];
            };
        } else {
            responseCallback = ^(id ignoreResponseData) {
                // Do nothing
            };
        }
        
        WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
        if (!handler) {
            NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
            continue;
        }
        //调用OC的Block,同时,如果OC调用responseCallback,则调用_queueMessage进行相应的处理
        handler(message[@"data"], responseCallback);
    }
}

这里先将返回的JSON字符串转换成对象,这里的字符串是调用

       function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    return messageQueueString;
   }

获取的,这里将sendMessageQueue转为JSON,然后将其置空,这里为啥使用数组而不用对象来存储呢?因为可能JS还没有处理结束就有两次调用,要保证他们不丢失使用了数组。然后判断数组中的Message对象是否有responseId(JS调用OC第一次时存储的),这里没有responseId所以走else:如果有callbackId(在JS中作为回调用的),定义responseCallback,这个block就是OC将处理结果返回给JS时用到的block。如果没有callbackId说明,不需要回调JS,这个时候responseCallback为空。最后调用步骤1中存储在messageHandlers对象中的block,并且将刚才创建的responseCallback作为参数传入,以便OC将计算结果传递给JS。

4. OC将计算结果返回给JS

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {

    NSLog(@"testObjcCallback called: %@", data);
    responseCallback(@"response form oc's call back");  
}];

handler的最后一步调用responseCallback()将处理结果回调给JS。这个responseCallback()就是我们在步骤3中创建的responseCallback。我们再来看这个block。看步骤3可以看到这个其内部调用

  [self _queueMessage:msg];
  [self _dispatchMessage:message];

_dispatchMessage内部执行:

 NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];

接下来JS中的_handleMessageFromObjC就会接收到OC传过来处理结果。

 function _doDispatchMessageFromObjC() {
        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 {
           // OC先调用JS是用到
        }
    }

这个时候我们看到了步骤三中的responseId的作用了,这时候responseId就表明了是OC将处理结果传递给JS并不需要JS再调用OC了,这时只调用responseCallback(message.responseData);将数据传给JS。
这样我们就完成了JS调用OC,然后OC将结果回调给JS的全部过程。

三、OC调用JS,然后JS将处理结果返回给OC

1. JS注册相应的方法供回调

同OC注册方法时候一样,JS也是用一个messageHandlers对象来存储

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

2. OC调用JS时存储调用信息

       - (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];
            self.responseCallbacks[callbackId] = [responseCallback copy];
            message[@"callbackId"] = callbackId;
        }
        
        if (handlerName) {
            message[@"handlerName"] = handlerName;
        }
        [self _queueMessage:message];
    }

这里使用message字典来存储参数,方法名,使用responseCallbacks来存储JS处理完之后,需要回调的Block(这里为了确保多次调用不会覆盖之前的调用,使用了唯一的callbackId)。
同上文所述,最终会调用

    - (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
    return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
    }

3. JS调用_dispatchMessageFromObjC

这时message没有responseId,会走else

    if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    handler(message.data, responseCallback);
                }
 这里定义了需要给OC传递结果的`responseCallback`,取出之前注册的`handler`:`messageHandlers[message.handlerName]`,然后调用这个`handler`,并将这个`responseCallback`作为参数传进去,`handler(message.data, responseCallback);`

4. JS将结果回传给OC

   在步骤三中调用handler:
   
       function(data, responseCallback) {
            log('ObjC called testJavascriptHandler with', data)
            var responseData = { 'Javascript Says':'Right back atcha!' }
            log('JS responding with', responseData)
            responseCallback(responseData)
        }
 在这个`handler`的结尾调用步骤三种的`responseCallback`(传入的只有数据没有回调),根据步骤三可以看出来其会调用`_doSend`方法。该方法中由于没有传进去回调,所以不会给message对象添加`callbackId`,只调用
 
       sendMessageQueue.push(message);
          messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;

这是由于含有responseId(在步骤三中的_doSend调用时设置),所以只会取出之前存储的block,并且将结果回传给OC:

        //调用之前存储的Bolck
        WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
        responseCallback(message[@"responseData"]);
        [self.responseCallbacks removeObjectForKey:responseId];

至此,OC和JS交互的所有逻辑已介绍完毕(WKWebView实现方式相同),总结下两种情景的回调,其实现方式及其相似,正如文章开头的总结。

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

推荐阅读更多精彩内容