WebViewJavascriptBridge源碼探祕(上)

1、咱們先看看如何使用OC調用JS中的方法。

注:咱們以wkwebview爲例。下面的代碼都是針對於wkwebview的。javascript

1.先建立一個按鈕和WKWebViewJavascriptBridge對象

UIButton *callbackButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [callbackButton setTitle:@"原生調用JS" forState:UIControlStateNormal];
    [callbackButton addTarget:self action:@selector(callJSMethod:) forControlEvents:UIControlEventTouchUpInside];
    ...
    _bridge = [WKWebViewJavascriptBridge bridgeForWebView:webView];

2.按鈕的消息處理函數

- (void)callJSMethod:(id)sender {
    id data = @{ @"原生調用JS參數1": @"參數1" };
    [_bridge callHandler:@"JSMethod1" data:data responseCallback:^(id response) {
        NSLog(@"testJavascriptHandler responded: %@", response);
    }];
}

3.調用WKWebViewJavascriptBridge中的callHandler方法

- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

4.WKWebViewJavascriptBridge中包含一個WebViewJavascriptBridgeBase對象_base。繼續調用棧的跟蹤。

- (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];
}

這個方法寫的很清晰,把要調用的js的函數名handlerName,參數data,和回調方法的Id(callbackId)打包到一個字典對象message中。callbackId,每一個回調一個,惟一。爲何用callbackId,由於block自己是一個對象這個對象JS識別不了。其實傳過去意義也不是很大,只要把這個block放在WebViewJavascriptBridgeBase對象中的responseCallbacks字典中就行,key就是剛纔生成的callbackId。而後繼續調用下面的方法。css

5.把消息放入隊列中。

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

這裏其實沒有放入隊列,而是直接分發了消息,稍後會說爲何這裏self.startupMessageQueue爲nil。html

6.把消息發送給Web環境

- (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"];
    
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

首先把WVJBMessage對象message串行化爲JSON字符串,而後轉義字符串裏的字符;生成JS的命令字符串;在主線程中執行js命令。爲何要在主線程中執行,蘋果文檔中有這麼一句話:The WebKit framework is not thread-safe. If you call functions or methods in this framework, you must do so exclusively on the main program thread。java

7.最終把命令傳給WebView來執行

//WebViewJavascriptBridgeBase.m
- (void) _evaluateJavascript:(NSString *)javascriptCommand {
    [self.delegate _evaluateJavascript:javascriptCommand];
}
//WKWebViewJavascriptBridge.ms
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    return NULL;
}

8.OC調用JS方法的本質

OC調用js的方法,都會把callback方法,方法名,參數打包到messageJSON中,而後調用下面這個終極方法。WebViewJavascriptBridge._handleMessageFromObjC(messageJSON);這個方法存在於WebViewJavascriptBridge_js.m文件中,是頁面加載的時候注入的。下面部分就講這個過程。web

2、方法如何在JS中執行

1.加載頁面的時候作了什麼?

咱們仍是以官方的例子爲例。加載一個本地的html頁面ExampleApp.html。加載以後以下方法會被執行json

- (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;

    if ([_base isWebViewJavascriptBridgeURL:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

這個方法是加載網頁第一個執行的方法,由於它要肯定是否容許或者取消加載這個導航(就是是否是容許加載這個頁面)。首次加載的時候url不是特殊的jsBridge的URL,直接容許加載這個頁面。下面看看頁面的源代碼。數組

2.被加載的頁面的內容(ExampleApp.html)

<!doctype html>
<html><head>
    <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <style type='text/css'>
        html { font-family:Helvetica; color:#222; }
        h1 { color:steelblue; font-size:24px; margin-top:24px; }
        button { margin:0 3px 10px; font-size:12px; }
        .logLine { border-bottom:1px solid #ccc; padding:4px 2px; font-family:courier; font-size:11px; }
    </style>
</head><body>
    <h1>WebViewJavascriptBridge Demo</h1>
    <script>
    window.onerror = function(err) {
        log('window.onerror: ' + err)
    }

    function setupWebViewJavascriptBridge(callback) {
        //第一次調用這個方法的時候,爲false
        if (window.WebViewJavascriptBridge) {
            var result = callback(WebViewJavascriptBridge);
            return result;
        }
        //第一次調用的時候,也是false
        if (window.WVJBCallbacks) {
            var result = window.WVJBCallbacks.push(callback);
            return result;
        }
        //把callback對象賦值給對象。
        window.WVJBCallbacks = [callback];
        //這段代碼的意思就是執行加載WebViewJavascriptBridge_JS.js中代碼的做用
        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 callback(bridge) {
        var uniqueId = 1
        //把操做記錄寫入webview中
        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)
            }
        }
        //把WEB中要註冊的方法註冊到bridge裏面
        bridge.registerHandler('JSMethod1', function(data, responseCallback) {
            log('OC調用JS方法成功', data)
            var responseData = { 'JS給OC調用的回調':'回調值!' }
            log('OC調用JS的返回值', responseData)
            responseCallback(responseData)
        })
        //獲取web中的button,而後添加點擊事件。
        document.body.appendChild(document.createElement('br'))
        document.getElementById('buttons').onclick = function(e) {
            e.preventDefault()
            var params =  {'JS調用OC參數': '參數值'};
            log('JS立刻調用OC方法',params)
            bridge.callHandler('OC提供方法給JS調用',params, function(response) {
                log('JS調用OC的返回值', response)
            })
        }
    };
    //驅動全部hander的初始化
    setupWebViewJavascriptBridge(callback);
    </script>
    <input type='button' id='buttons' class='button' value='點擊開始JS調用OC'></input>
    <div id='log'></div>
</body></html>

定義了2個方法,而且以第二個方法爲參數,調用了第一個方法。方法一第一次調用的時候只是爲window對象添加了一個數組WVJBCallbacks。並把第二個函數放進去。而後建立一個不可見的iframe元素,設置其url爲一個特殊的url:https://__bridge_loaded__。這樣也面又會發起一個請求,第一步的方法webView: decidePolicyForNavigationAction: decisionHandler再次被調用。此次會執行到這個分支[_base injectJavascriptFile]。app

3.這一次要把WebViewJavascriptBridge_js裏面的代碼注入到js中

- (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];
        }
    }
}

WebViewJavascriptBridge_js只包含一個方法,生成一個字符。這個字符串就是要注入的js代碼(也就是要執行的的代碼)。框架

NSString * WebViewJavascriptBridge_js() {
    #define __wvjb_js_func__(x) #x
    
    // BEGIN preprocessorJSCode
    static NSString * preprocessorJSCode = @__wvjb_js_func__(
;(function() {
    if (window.WebViewJavascriptBridge) {
        return;
    }

    if (!window.onerror) {
        window.onerror = function(msg, url, line) {
            console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
        }
    }
    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) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    function disableJavscriptAlertBoxSafetyTimeout() {
        dispatchMessagesWithTimeoutSafety = false;
    }
    
    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;
    }

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

    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);
    
    setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }
})();
    ); // END preprocessorJSCode

    #undef __wvjb_js_func__
    return preprocessorJSCode;
};

這個js代碼的功能都是啥?建立了window.WebViewJavascriptBridge對象,這個是整個OC和原生交互的核心。這個對象裏面包含方法_handleMessageFromObjC。就是咱們第一部分8中調用的。這個方法會調用_doDispatchMessageFromObjC()方法。而後就是定義了各類對象,和函數。在最後咱們看到一個經過setTimeout的函數調用。ide

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

還記得window.WVJBCallbacks嗎?他是在ExampleApp.html中定義的一個存儲回調的數組。咱們定義的callback就放在裏面。調用這個數組裏的全部回調函數並以WebViewJavascriptBridge對象做爲參數。因而在ExampleApp.html中定義的第二個方法獲得執行(這個方法裏包含用戶頁面要執行的js代碼,因此要放到頁面裏,不能放到框架jsbridge中)。
看看callback方法裏的這段代碼。

//把WEB中要註冊的方法註冊到bridge裏面
        bridge.registerHandler('JSMethod1', function(data, responseCallback) {
            log('OC調用JS方法成功', data)
            var responseData = { 'JS給OC調用的回調':'回調值!' }
            log('OC調用JS的返回值', responseData)
            responseCallback(responseData)
        })

調用了bridge對象的registerHandler註冊了一個方法名和對應的函數,OC就是經過這個方法名JSMethod1來調用了JS的方法。咱們回到WebViewJavascriptBridge_js中,看registerHandler它是如何實現的。

function registerHandler(handlerName, handler) {
        alert(handlerName+'01');
        messageHandlers[handlerName] = handler;
    }

就是把函數存到了對象messageHandlers裏。到此OC要調用JS方法已經放到字典裏,等待被調用。

4.OC調用JS方法

第一部分第8步裏說過,OC調用JS方法最後都會變成執行WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)這個在js代碼中定義的方法。下面仍是看源碼吧

function _handleMessageFromObjC(messageJSON) {
    _dispatchMessageFromObjC(messageJSON);
}
//繼續看_dispatchMessageFromObjC
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 {
                
                //alert(message.callbackId);        //objc_cb_1 objc_cb_2 objc_cb_3....
                
                //若是OC調用JS的時候設置了回調用的block,callbackId就不爲空。這裏生成了一個responseCallback函數。在後面調用JS方法的時候用。
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    
                    //這裏定義的這個函數,被傳遞給OC將要調用的JS方法。在方法裏會用要傳給OC的數據responseData作參數調用。若是沒有定義這個回調函數,OC調用JS方法也能成功,可是調用的時候傳入的block不會被執行。
                    responseCallback = function(responseData) {
                        //這裏只傳遞了一個參數。
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                        
                    };
                }
                
                //messageHandlers字典中存着咱們要調用的JS的方法。
                var handler = messageHandlers[message.handlerName];
                alert(message.handlerName);
                
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    //調用JS的方法,至此OC最終調用了JS的方法。傳入的responseCallback方法在JS中被調用,參數是要返回給OC的數據  yuxg
                    handler(message.data, responseCallback);
                }
            }
        }
    }

5.JS經過調用OC回調block,來給OC傳回值。

這把message對象放到數組裏,而後更改iframe的url,刷新頁面。
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message['callbackId'] = callbackId;
    }
    //message對象裏包含,OC調用的JS的方法名,回調的Block的Id,和block的參數,也就是傳回的數據。
    //把這個對象
    sendMessageQueue.push(message);
    //把iframe的地址修改成:https://__wvjb_queue_message__
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

JS給OC傳遞消息,都是經過修改iframed的src(也就是url)來實現的。這樣WKWebViewJavascriptBridge裏面webView:decidePolicyForNavigationAction: decisionHandler方法就能攔截這個消息。在裏面會調用
[self WKFlushMessageQueue],咱們繼續看代碼

//WKWebViewJavascriptBridge.m
- (void)WKFlushMessageQueue {
    [_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];
    }];
}
//WebViewJavascriptBridgeBase.m
 (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"];
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            //這裏調用了OC調用JS方法是傳入的block。
             responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            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
                };
            }
            
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            
            handler(message[@"data"], responseCallback);
        }
    }
}
相關文章
相關標籤/搜索