WebViewJavascriptBridge源碼剖析

對(duì)于任意hybrid APP,不可避免進(jìn)行native與web之間的交互。WebViewJavascriptBridge 就是一款用于實(shí)現(xiàn)原生端與web端無(wú)縫交互的三方庫(kù),應(yīng)用廣泛,支持UIWebView、WKWebView(iOS)以及WebView(OSX),原理一致,本文借助OC的UIWebView進(jìn)行分析。

框架簡(jiǎn)介

所謂交互,無(wú)非就是兩端(native端與JS端)能夠互相調(diào)用方法,并傳遞數(shù)據(jù)。iOS7之后,隨著JavaScriptCore框架的推出,為native與JS的交互鋪平了道路,使用該框架即可實(shí)現(xiàn)OC與JS的互調(diào)。而在iOS7之前,OC與jS的交互只能借助于WebView,OC調(diào)用JS依賴于WebView提供的執(zhí)行JS的方法,而JS調(diào)用OC則需要采用URL攔截的方式。WebViewJavascriptBridge就是為WebView中的JS交互而生的,采用后者的交互方式。

大致原理為OC端和JS端各自保存一個(gè)bridge對(duì)象,并各自維護(hù)開(kāi)放給另一端調(diào)用的方法集合以及回調(diào)方法集合,兩端之間的交互通過(guò)傳遞handleName(可以理解為方法id)以及callBackId來(lái)實(shí)現(xiàn)方法調(diào)用以及回調(diào)的。

代碼文件結(jié)構(gòu)如下(V6.0.2版本):

  • WebViewJavascriptBridge
    基于UIWebView/WebView的OC端交互邏輯處理類,面向OC業(yè)務(wù)層,提供了注冊(cè)O(shè)C方法、調(diào)用JS方法等接口。

  • WebViewJavascriptBridgeBase
    OC端bridge對(duì)象基礎(chǔ)服務(wù)類,維護(hù)OC端開(kāi)放給JS端的方法以及OC回調(diào)方法,實(shí)現(xiàn)OC向JS發(fā)送數(shù)據(jù)的具體邏輯。

  • WebViewJavascriptBridge_JS
    維護(hù)了一份JS代碼,用于JS環(huán)境的注入。同時(shí)維護(hù)JS端的bridge對(duì)象,管理JS端注冊(cè)的方法集合以及回調(diào)方法集合,面向Web端提供注冊(cè)JS方法、調(diào)用OC端方法的接口。

  • WKWebViewJavascriptBridge
    基于WKWebView的OC端交互邏輯處理類,職能同WebViewJavascriptBridge。

  • ExampleApp.html
    非框架文件,由于我們借助WebViewJavascriptBridge的官方example講解,該文件作為模擬開(kāi)發(fā)環(huán)境的web頁(yè)面示例。

Bridge環(huán)境初始化

OC端bridge初始化

業(yè)務(wù)層創(chuàng)建好webView實(shí)例之后,需要根據(jù)此webView創(chuàng)建對(duì)應(yīng)的Bridge,即初始化bridge對(duì)象:

//初始化OC端Bridge對(duì)象
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
//設(shè)置代理,避免框架對(duì)webView代理的入侵
[_bridge setWebViewDelegate:self];

相關(guān)實(shí)現(xiàn)如下(僅保留相關(guān)代碼):

+ (instancetype)bridgeForWebView:(id)webView {
    return [self bridge:webView];
}

+ (instancetype)bridge:(id)webView {
    if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
        WebViewJavascriptBridge* bridge = [[self alloc] init];
        [bridge _platformSpecificSetup:webView];
        return bridge;
    }
    return nil;
}

- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
    _webView = webView;
    _webView.delegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

//WebViewJavascriptBridgeBase實(shí)例初始化
//messageHandlers:保存OC端注冊(cè)給JS端的方法集合,key為handleName,value為方法實(shí)現(xiàn)block
//startupMessageQueue:由于OC端調(diào)用JS方法時(shí),JS環(huán)境可能還沒(méi)有初始化好,該數(shù)組暫存JS環(huán)境初始化之前的JS方法調(diào)用操作(保存調(diào)用的handleName)
//responseCallbacks:保存OC端調(diào)用JS方法的回調(diào)block,key為callBackId,value會(huì)回調(diào)實(shí)現(xiàn)block
//_uniqueId:表示消息的唯一性
- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    return self;
}

OC端的bridge初始化即為webView實(shí)例創(chuàng)建了一個(gè)bridge對(duì)象,并初始化了相關(guān)的數(shù)據(jù)結(jié)構(gòu)。

JS端Bridge初始化

JS端bridge對(duì)象的初始化要比OC端復(fù)雜得多,因?yàn)镴S環(huán)境的初始化也是在原生端完成的,涉及原生端對(duì)webView初始化JS環(huán)境操作指令的捕捉,以及JS代碼的注入。

webView加載時(shí),執(zhí)行了如下JS:

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

傳入的參數(shù)是一個(gè)函數(shù),暫且不管該函數(shù)的內(nèi)容。首先判斷當(dāng)前環(huán)境中是否存在WebViewJavascriptBridge對(duì)象,若存在則直接調(diào)用傳入的函數(shù)并已WebViewJavascriptBridge作為參數(shù)。該WebViewJavascriptBridge就是JS端的bridge對(duì)象,頁(yè)面首次加載時(shí),JS環(huán)境沒(méi)有初始化好,也就是該Bridge對(duì)象沒(méi)有創(chuàng)建好,因此會(huì)先將傳入的函數(shù)暫存在數(shù)組WVJBCallbacks中。

重點(diǎn)看 WVJBIframe.src = 'https://__bridge_loaded__'; ,iframe可以理解為webView的窗口,當(dāng)改變iframe的src時(shí),頁(yè)面就會(huì)進(jìn)行刷新并加載指定的URL,此處試圖加載 https://__bridge_loaded__。

我們知道,UIWebView刷新頁(yè)面前,會(huì)先回調(diào) shouldStartLoadWithRequest 方法。因此可以在該代理方法中攔截該URL的加載,并執(zhí)行相應(yīng)的邏輯??梢栽?WebViewJavascriptBridge.m 文件中看到如下代碼:

- (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;
    
    //判斷是否是WebViewJavascriptBridge內(nèi)部發(fā)起的請(qǐng)求
    if ([_base isWebViewJavascriptBridgeURL:url]) {
        //判斷是否是JS環(huán)境初始化請(qǐng)求
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        }
        //判斷是否是web端交互請(qǐng)求
        else if ([_base isQueueMessageURL:url]) {
            NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            [_base flushMessageQueue:messageQueueString];
        }
        //未知請(qǐng)求
        else {
            [_base logUnkownMessage:url];
        }
        return NO;
    }
    //常規(guī)webView加載請(qǐng)求,交給業(yè)務(wù)層的webView自行處理
    else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    } else {
        return YES;
    }
}

重點(diǎn)看中間部分,主要攔截了兩種請(qǐng)求:JS環(huán)境初始化請(qǐng)求和web端交互請(qǐng)求,都是由WebViewJavascriptBridge內(nèi)部發(fā)起的。

#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage   @"__wvjb_queue_message__"
#define kBridgeLoaded      @"__bridge_loaded__"

//判斷是否是WebViewJavascriptBridge內(nèi)部發(fā)起的請(qǐng)求
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url {
    if (![self isSchemeMatch:url]) {
        return NO;
    }
    return [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url];
}

//判斷請(qǐng)求的協(xié)議是否符合WebViewJavascriptBridge的約定
- (BOOL)isSchemeMatch:(NSURL*)url {
    NSString* scheme = url.scheme.lowercaseString;
    return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
}

//判斷是否是WebViewJavascriptBridge發(fā)起的web交互請(qǐng)求
- (BOOL)isQueueMessageURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}

//判斷是否是WebViewJavascriptBridge發(fā)起的JS環(huán)境初始化請(qǐng)求
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}

因此,之前頁(yè)面中加載的 https://__bridge_loaded__ 就會(huì)被攔截,并識(shí)別為JS環(huán)境初始化請(qǐng)求,由OC端執(zhí)行相應(yīng)的的初始化邏輯。

- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

該方法中執(zhí)行了WebViewJavascriptBridge_JS文件中的js代碼。為方便閱讀,我把JS代碼拎出來(lái)如下:

;(function() {
    if (window.WebViewJavascriptBridge) {
        return;
    }
    if (!window.onerror) {
        window.onerror = function(msg, url, line) {
            console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
        }
    }
    //創(chuàng)建Bridge對(duì)象
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };

    //初始化一些變量
    var messagingIframe;
    //消息隊(duì)列,存放發(fā)送給OC的數(shù)據(jù)
    var sendMessageQueue = [];
    //存放JS端注冊(cè)的函數(shù)
    var messageHandlers = {};
    
    var CUSTOM_PROTOCOL_SCHEME = 'https';
    var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
    
    //存放回調(diào)函數(shù)
    var responseCallbacks = {};
    //標(biāo)識(shí)消息的唯一性
    var uniqueId = 1;
    //是否異步執(zhí)行
    var dispatchMessagesWithTimeoutSafety = true;

    //注冊(cè)給OC調(diào)用的方法
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    
    //調(diào)用OC方法
    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }

    //禁止異步發(fā)送消息
    function disableJavscriptAlertBoxSafetyTimeout() {
        dispatchMessagesWithTimeoutSafety = false;
    }

    //向OC發(fā)送消息
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
            //responseCallBacks對(duì)象對(duì)用callBackId存放回調(diào)函數(shù)
            responseCallbacks[callbackId] = responseCallback;
            message['callbackId'] = callbackId;
        }
        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

    //獲取當(dāng)前消息隊(duì)列待發(fā)送的消息
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }

    //接收從OC發(fā)來(lái)的消息
    function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
             _doDispatchMessageFromObjC();
        }
        
        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 {
                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);
                }
            }
        }
    }
    
    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);
    
    //Bridge初始化完畢后,執(zhí)行webView加載時(shí)待執(zhí)行的JS代碼
    setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }
})();

分析一下,所做的事情很簡(jiǎn)單,就是創(chuàng)建了一個(gè)bridge對(duì)象,該對(duì)象維護(hù)了若干函數(shù),OC端可以通過(guò)bridge對(duì)象獲取到相應(yīng)的函數(shù)來(lái)執(zhí)行調(diào)用。

  • registerHandler
    用于JS端注冊(cè)供OC端調(diào)用的方法

  • callHandler
    用于調(diào)用OC方法

  • disableJavscriptAlertBoxSafetyTimeout
    關(guān)閉消息異步發(fā)送

  • _fetchQueue
    獲取當(dāng)前JS端待發(fā)送的消息

  • _handleMessageFromObjC
    接收并處理OC端發(fā)送給JS端的數(shù)據(jù)

另外,代碼最后執(zhí)行了_callWVJBCallbacks 函數(shù) ,還記得之前webView加載時(shí)暫存在WVJBCallbacks的函數(shù)嗎?那時(shí)因?yàn)閎ridge未初始化先暫存起來(lái),現(xiàn)在bridge初始化完畢了再來(lái)執(zhí)行。

至此JS環(huán)境初始化完成。不過(guò)上面 injectJavascriptFile 方法中除了執(zhí)行JS代碼,還執(zhí)行了如下代碼:

if (self.startupMessageQueue) {
    NSArray* queue = self.startupMessageQueue;
    self.startupMessageQueue = nil;
    for (id queuedMessage in queue) {
        [self _dispatchMessage:queuedMessage];
    }
}

startupMessageQueue是OC端bridge維護(hù)的屬性,之前介紹過(guò)其用于暫存JS環(huán)境初始化之前OC發(fā)起的JS消息調(diào)用?,F(xiàn)在JS環(huán)境初始化好了,便可取出其中的消息發(fā)送了。

OC調(diào)用JS

OC調(diào)用JS可以分為4個(gè)過(guò)程:

  1. JS注冊(cè)方法給OC
  2. OC調(diào)用JS注冊(cè)的方法
  3. JS方法被調(diào)用,執(zhí)行自身邏輯
  4. JS回調(diào)數(shù)據(jù)給OC

JS注冊(cè)方法

毫無(wú)疑問(wèn),這一步應(yīng)該在webView加載時(shí)執(zhí)行。還記得ExampleApp.html在加載時(shí)傳了一個(gè)函數(shù)給 setupWebViewJavascriptBridge() 嗎。這個(gè)函數(shù)后面被執(zhí)行,函數(shù)內(nèi)容就腦闊相關(guān)JS方法的注冊(cè)。示例中該函數(shù)相關(guān)內(nèi)容如下:

function(bridge) {
    ...
    //注冊(cè)testJavascriptHandler方法
    bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
        log('ObjC called testJavascriptHandler with', data)
        var responseData = { 'Javascript Says':'Right back atcha!' }
        log('JS responding with', responseData)
        responseCallback(responseData)
    })
    ...
}

沒(méi)錯(cuò),就是通過(guò)調(diào)用bridge對(duì)象的registerHandler方法來(lái)注冊(cè)的。實(shí)現(xiàn)很簡(jiǎn)單:

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

單純只是把handleName及方法實(shí)現(xiàn)綁定在一起,保存在messageHandlers中。

OC發(fā)起調(diào)用

OC端通過(guò)JS注冊(cè)的handleName調(diào)用JS相應(yīng)的方法,同時(shí)可以設(shè)置回調(diào)操作。

[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" } responseCallback:^(id responseData) {
    NSLog(@"testJavascriptHandler callBack: %@", responseData);
}];
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    if (data) {
        message[@"data"] = data;
    }
    //若OC端需要回調(diào),則創(chuàng)建唯一的callbackId保存該回調(diào)block
    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        self.responseCallbacks[callbackId] = [responseCallback copy];
        message[@"callbackId"] = callbackId;
    }
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    //OC端將數(shù)據(jù)封裝成特定格式的字典,JS端在拿到數(shù)據(jù)后按照該格式解封裝
    [self _queueMessage:message];
}

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        //若startupMessageQueue不為空,說(shuō)明JS環(huán)境未初始化,暫存消息
        [self.startupMessageQueue addObject:message];
    } else {
        //直接發(fā)送消息
        [self _dispatchMessage:message];
    }
}

- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    [self _log:@"SEND" json:messageJSON];
    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"];
    
    //序列化消息數(shù)據(jù)并進(jìn)行轉(zhuǎn)義后,通過(guò)webView的stringByEvaluatingJavaScriptFromString執(zhí)行JS代碼。JS內(nèi)容為調(diào)用bridge對(duì)象的_handleMessageFromObjC方法,并傳入序列化后的數(shù)據(jù)
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];
    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

JS接收調(diào)用

JS通過(guò)bridge的 _handleMessageFromObjC 方法接收OC的消息,具體實(shí)現(xiàn)如下:

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

function _dispatchMessageFromObjC(messageJSON) {
    //根據(jù)dispatchMessagesWithTimeoutSafety標(biāo)志量決定是否異步執(zhí)行
    if (dispatchMessagesWithTimeoutSafety) {
        setTimeout(_doDispatchMessageFromObjC);
    } else {
         _doDispatchMessageFromObjC();
    }
    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;

        //若responseId不為空,說(shuō)明這是一個(gè)回調(diào)消息
        if (message.responseId) 
        {
            //從responseCallbacks中取出事先保存好的回調(diào)函數(shù)執(zhí)行
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            //執(zhí)行完畢后移除回調(diào)函數(shù)
            delete responseCallbacks[message.responseId];
        } 
        //若responseId不存在,說(shuō)明這是一個(gè)OC發(fā)起的消息
        else 
        {
            //若callbackId不為空,說(shuō)明需要回調(diào)OC
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                //創(chuàng)建回調(diào)函數(shù),傳入JS方法中,由web業(yè)務(wù)方?jīng)Q定何時(shí)執(zhí)行該回調(diào)函數(shù)來(lái)回調(diào)OC
                responseCallback = function(responseData) {
                    //通過(guò)調(diào)用_doSend向OC發(fā)送包含responseId的數(shù)據(jù),來(lái)指明這是一個(gè)回調(diào)調(diào)用,responseId的值為OC傳過(guò)來(lái)的callbackId。
                    _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                };
            }
            //從messageHandlers中根據(jù)handlerName取出之前注冊(cè)的方法
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            } else {
                //執(zhí)行JS方法自身邏輯
                handler(message.data, responseCallback);
            }
        }
    }
}

首先解析OC傳過(guò)來(lái)的數(shù)據(jù),然后判斷數(shù)據(jù)中是否存在responseId字段,若存在,則被認(rèn)為這是一個(gè)OC回調(diào)的消息,否則被認(rèn)為是OC發(fā)起的消息。此處不存在responseId,因?yàn)楫?dāng)前場(chǎng)景為OC發(fā)起的消息。但是因?yàn)橄?shù)據(jù)中含有callbackId,被認(rèn)為是OC需要回調(diào),因此創(chuàng)建一個(gè)callBack函數(shù),函數(shù)內(nèi)容為調(diào)用 _doSend 向OC發(fā)送指定格式的數(shù)據(jù)(重點(diǎn)是包含responseId字段,OC端會(huì)根據(jù)此字段識(shí)別這是一個(gè)JS回調(diào)調(diào)用,同JS端)。隨后,根據(jù)OC傳過(guò)來(lái)的handleName獲取到注冊(cè)的方法,傳入data及callBack函數(shù)來(lái)調(diào)用。至此,web端的方法得到調(diào)用:

bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
    log('ObjC called testJavascriptHandler with', data)
    var responseData = { 'Javascript Says':'Right back atcha!' }
    log('JS responding with', responseData)
    responseCallback(responseData)
})

OC回調(diào)

JS注冊(cè)的方法被調(diào)用的時(shí)候,接收了一個(gè)回調(diào)OC的函數(shù),業(yè)務(wù)端根據(jù)實(shí)際情況選擇合適的時(shí)機(jī)調(diào)用該函數(shù)以回調(diào)數(shù)據(jù)給OC端。關(guān)于該回調(diào)函數(shù)的創(chuàng)建上面已經(jīng)有說(shuō)明,回調(diào)的方式就是向OC發(fā)送消息,屬于JS調(diào)用OC的范疇,將在下面介紹。

JS調(diào)用OC

和OC調(diào)用JS一樣,JS調(diào)用OC也是同樣的4個(gè)過(guò)程:

  1. OC注冊(cè)方法給JS
  2. JS發(fā)起調(diào)用
  3. OC接收調(diào)用并執(zhí)行相應(yīng)邏輯
  4. OC回調(diào)數(shù)據(jù)給JS

OC注冊(cè)方法

和JS類似,OC端也是通過(guò)bridge對(duì)象注冊(cè)方法。

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"testObjcCallback called: %@", data);
    responseCallback(@"Response from testObjcCallback");
}];

實(shí)現(xiàn)也一致,綁定handleName與block,保存到bridge的messageHandlers屬性中:

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

JS發(fā)起調(diào)用

web業(yè)務(wù)端根據(jù)實(shí)際場(chǎng)景調(diào)用OC,示例中是在ExampleApp.html加載時(shí)向DOM中添加了一個(gè)按鈕元素,點(diǎn)擊按鈕調(diào)用OC注冊(cè)的方法。

var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
    e.preventDefault()
    log('JS calling handler "testObjcCallback"')
    bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
        log('JS got response', response)
    })
} 

可以看到,JS端通過(guò)bridge對(duì)象的 callHandler 執(zhí)行調(diào)用。和OC調(diào)用JS很類似,也是傳了3個(gè)參數(shù):handleName、data、回調(diào)函數(shù)。相關(guān)方法實(shí)現(xiàn)如下:

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

function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
        //responseCallBacks對(duì)象對(duì)用callBackId存放回調(diào)函數(shù)
        responseCallbacks[callbackId] = responseCallback;
        message['callbackId'] = callbackId;
    }
    sendMessageQueue.push(message);
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

可以看到最終執(zhí)行的是 _doSend 函數(shù),這個(gè)函數(shù)之前在講解JS回調(diào)OC的時(shí)候也遇到過(guò),其作用就是向OC發(fā)送消息。_doSend 執(zhí)行時(shí),若JS需要回調(diào),即responseCallback參數(shù)不為空,則會(huì)創(chuàng)建唯一callbackId,并保存回調(diào)函數(shù)。隨后,將數(shù)據(jù)封裝成指定格式的對(duì)象(格式同OC調(diào)用JS時(shí)一致),發(fā)送給OC。此處,我們又見(jiàn)到了眼熟的src,之前在web端初始化JS環(huán)境的時(shí)候遇到過(guò)。沒(méi)錯(cuò),JS無(wú)法直接調(diào)用OC,此處依舊是通過(guò)URL攔截的方式實(shí)現(xiàn)的。首先將要發(fā)送給OC的數(shù)據(jù)保存在全局變量中,然后修改iframe的src來(lái)加載特定URL:https://__wvjb_queue_message__

OC接收調(diào)用

因?yàn)閣ebView要加載URL,同樣先回調(diào)了webView的 shouldStartLoadWithRequest 方法。方法中識(shí)別了這是一個(gè)由WebViewJavascriptBridge發(fā)起的web端交互請(qǐng)求,進(jìn)行攔截處理:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    ...
    if ([_base isQueueMessageURL:url]) {
        NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
        [_base flushMessageQueue:messageQueueString];
    }
    ...
}

其中 webViewJavascriptFetchQueyCommand 的作用的獲取之前保存在JS端的需要傳遞給OC的數(shù)據(jù):

- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}

通過(guò)JS端bridge對(duì)象的 _fetchQueue 獲?。?/p>

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

可以看到傳過(guò)來(lái)的數(shù)據(jù)是一個(gè)被序列化的數(shù)組字符串。

OC端拿到數(shù)據(jù)后,調(diào)用 flushMessageQueue 執(zhí)行相應(yīng)的邏輯。

- (void)flushMessageQueue:(NSString *)messageQueueString{
    if (messageQueueString == nil || messageQueueString.length == 0) {
        NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
        return;
    }

    //反序列化json
    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];
        NSString* responseId = message[@"responseId"];
        //若傳過(guò)來(lái)的數(shù)據(jù)中包含responseId字段,說(shuō)明這是一個(gè)JS回調(diào)調(diào)用
        if (responseId)
        {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        }
        //若數(shù)據(jù)中不含responseId,說(shuō)明這是一個(gè)JS發(fā)起的調(diào)用
        else
        {
            WVJBResponseCallback responseCallback = NULL;
            //若數(shù)據(jù)中含callbackId,說(shuō)明JS端需要回調(diào)
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) {
                //創(chuàng)建回調(diào)JS的Block
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    //向JS發(fā)送特定格式的數(shù)據(jù)來(lái)執(zhí)行回調(diào)
                    [self _queueMessage:msg];
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            //根據(jù)handlerName獲取messageHandlers中保存的方法
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            
            //執(zhí)行OC方法
            handler(message[@"data"], responseCallback);
        }
    }
}

處理邏輯和JS接收數(shù)據(jù)后的處理邏輯幾乎一致,同樣是根據(jù)傳過(guò)來(lái)的數(shù)據(jù)中是否包含responseId字段來(lái)判斷這是一個(gè)JS回調(diào)調(diào)用還是由JS發(fā)起的調(diào)用。若是JS回調(diào)調(diào)用,則從responseCallbacks集合中根據(jù)responseId取出回調(diào)block并調(diào)用;若是JS發(fā)起的調(diào)用,則最終從messageHandlers中根據(jù)handlerName取出對(duì)應(yīng)的block調(diào)用。

JS回調(diào)

OC端在處理JS的調(diào)用時(shí),若識(shí)別出這是一個(gè)JS發(fā)起的調(diào)用,而非回調(diào)調(diào)用,則根據(jù)傳過(guò)來(lái)的數(shù)據(jù)中是否包含callbackId字段來(lái)決定是否需要回調(diào)JS。若需要,則創(chuàng)建回調(diào)block,并將該回調(diào)block傳入OC方法,由OC業(yè)務(wù)層決定何時(shí)進(jìn)行回調(diào),以及回調(diào)什么樣的數(shù)據(jù)。而該回調(diào)block的內(nèi)容,無(wú)可厚非就是向JS發(fā)送特定格式的數(shù)據(jù):

responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    //向JS發(fā)送特定格式的數(shù)據(jù)來(lái)執(zhí)行回調(diào)
                    [self _queueMessage:msg];
                };

_queueMessage 方法正是之前介紹的OC調(diào)用JS的方法。另外,JS在接收到回調(diào)數(shù)據(jù)后的處理也在之前介紹過(guò)了,不再贅述。

總結(jié)

  • 交互前需要先對(duì)OC環(huán)境和JS環(huán)境進(jìn)行初始化,JS環(huán)境的初始化通過(guò)Web頁(yè)面加載時(shí)發(fā)送特定的URL來(lái)完成。
  • WebViewJavascriptBridge在OC端和JS端各自維護(hù)一個(gè)bridge對(duì)象來(lái)保存開(kāi)放給另一端的方法,以及自身調(diào)用另一端后的回調(diào)方法。前者通過(guò)handlerName來(lái)映射,后者通過(guò)callBackId標(biāo)識(shí)唯一性。方法調(diào)用時(shí)必定攜帶handlerName,若需要回調(diào),還需攜帶callBackId。
  • WebViewJavascriptBridge中OC調(diào)用JS采用的是WebView提供的JS執(zhí)行方法;而JS調(diào)用OC采用的是URL攔截的方式,OC端通過(guò)識(shí)別特定的URL來(lái)區(qū)分是否需要攔截,并做相應(yīng)的邏輯處理。
最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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