iOS WebviewJavascriptBridge 源碼研讀筆記

這兩天接近元旦,事情稍微少些,有些時間,索性寫點什麼,就從最擅長的iOS混合開發寫起了,因爲iOS開發經驗不到四年吧,期間還搞了一年半的前端,有些知識可能仍是積累的不足,能力不足,水平有限,可能有謬誤但願各位讀者發現的話及時指正,感激涕零。html

至於WebviewJavascriptBridge的介紹,此處再也不囉嗦了,既然能看到本文,相比對該三方庫或多或少仍是有所瞭解的吧。我在申明一點,本文中涉及的demo是直接拿的WebviewJavascriptBridge的,並未作任何修改,直接拿來研究前端

看了看比較流行的WebviewJavascriptBridge這個三方庫的源碼,發現好多js和oc部分的核心代碼幾乎是對稱的,因此以爲最好是js和oc代碼一塊兒讀,這樣才更容易理解,也能發現其對稱美。。。java

要搞明白其調用邏輯,最好是用Safari連上調試一把哈,在網頁檢查其中咱們用oc載入的js代碼好難找啊(至少我是花了一番功夫才找到了),莫慌,是在找不到的話在搜索欄裏面搜一下WebviewJavascriptBridge,而後在對應的代碼出都打上斷點,這下就能夠研究了git

至於有些同窗不知道如何打開Safari的調試模式的,請移步至傳送門這個方法mac 的Safari也一樣受用哈github

WebViewJavascriptBridge VS WKWebViewJavascriptBridge

這個框架仍是有點666啊,既支持iOS又支持mac OS 但鑑於咱們mac OS 用的少,就直接看iOS部分了 web

紅線框出來的部分也就是就是WebviewJavascriptBridge框架的核心代碼部分objective-c

WebViewJavascriptBridge_JS ==> js核心代碼部分,負責js端消息的組裝,轉發 WebViewJavascriptBridgeBase ==> oc核心代碼部分,負責oc端消息組裝,轉發 WebViewJavascriptBridge ==> 對於UIWebView的進行的封裝,是基於WebViewJavascriptBridgeBase的 WKWebViewJavascriptBridge ==> 對於WKWebView的進行的封裝,是基於WebViewJavascriptBridgeBase的json

至於前面兩個核心類會在下一小節中作詳細的闡述,本小結就只作後面兩個類的分析闡述 直接上圖了 數組

對比WebViewJavascriptBridgeWKWebViewJavascriptBridge兩個類的頭文件,看處WKWebViewJavascriptBridge多了一個reset方法,其餘的方法兩個類幾乎一毛同樣,咱們繼續看.m實現文件也證明了這一點,差異僅在於webview的實現,這也印證了這個框架的核心只是WebViewJavascriptBridgeBase,其核心都是經過js中去「loadUrl」(這個是本人本身習慣這麼說,方便理解啊,實際上和loadUrl有點差異,不過道理是同樣的)而後webview在代理方法中去攔截特殊約定好的url,而後進行消息的處理。bash

如下是wkwebview的代理方法截取

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    //獲取js 「loadUrl」的url連接
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    if ([_base isWebViewJavascriptBridgeURL:url]) {//是否是WebViewJavascriptBridge約定的url
        if ([_base isBridgeLoadedURL:url]) {//是否是初始化指令__bridge_loaded__
            //注入核心js代碼
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {//是否是消息指令__wvjb_queue_message__
            //調用WebViewJavascriptBridgeBase的API去分發消息
            [self WKFlushMessageQueue];
        } else {//未知的url
            [_base logUnkownMessage:url];
        }
        //取消跳轉
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    //能走到這裏證實已經不是WebViewJavascriptBridge約定的url了,作正常跳轉
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}
複製代碼

如下是UIWebView代理的方法

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if (webView != _webView) { return YES; }
    //獲取js 「loadUrl」的url連接
    NSURL *url = [request URL];
    __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
    if ([_base isWebViewJavascriptBridgeURL:url]) {//是否是WebViewJavascriptBridge約定的url
        if ([_base isBridgeLoadedURL:url]) {//是否是初始化指令__bridge_loaded__
            //注入核心js代碼
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {//是否是消息指令__wvjb_queue_message__
            NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            //調用WebViewJavascriptBridgeBase的API去分發消息
            [_base flushMessageQueue:messageQueueString];
        } else {//未知的url
            [_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;
    }
}
複製代碼

js 調用oc

//js

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

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

UIWebView裏面還看到了mac OS平臺的處理,其實質跟這個也是同樣的,有興趣的同窗能夠自行研究啊。

因爲UIWebView和WKWeb到WebViewJavascriptBridgeBase層的實現原理什麼的基本上是一致的,我這裏就以WKWebView精心給講解了

WebViewJavascriptBridgeBase的實現分析

前文已經說過,該框架裏面有好多地方oc和js是相對稱的,有不少相似的實現,如今就先引用幾個對比一下

這個是註冊handler的方法

//js
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		
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
        NSLog(@"testJavascriptHandler responded: %@", response);
    }];
複製代碼

js調用oc和oc調用js時候都各自維護了一套消息對列隊列,回調

var messageHandlers = {}; //消息
var responseCallbacks = {}; //回調


@property (strong, nonatomic) NSMutableDictionary* responseCallbacks;
@property (strong, nonatomic) NSMutableDictionary* messageHandlers;
複製代碼

相互交互的消息內容

//這是js發給oc的
{
    callbackId = "cb_1_1514520891115";
    data =     {
        foo = bar;
    };
    handlerName = testObjcCallback;
}

//這是oc發給js的
{
    callbackId = "objc_cb_1";
    data =     {
        greetingFromObjC = "Hi there, JS!";
    };
    handlerName = testJavascriptHandler;
}

複製代碼

send方法也跟雙胞胎似的,傻傻的不清楚啊

//js

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

複製代碼

下面來看一看咱們大Objective-c 調用JavaScript部分的實現過程

一、原生按鈕回調bridge的方法- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback將消息發出去

- (void)callHandler:(id)sender {
    id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
    [_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
        NSLog(@"testJavascriptHandler responded: %@", response);
    }];
}
複製代碼

二、調用WebViewJavascriptBridgeBase的方法- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName去組一波數據

- (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];
}
複製代碼

send 方法將oc傳個js的數據組裝成特定的json格式,以下所示:

{
    callbackId = "objc_cb_1";
    data =     {
        greetingFromObjC = "Hi there, JS!";
    };
    handlerName = testJavascriptHandler;
}
複製代碼

三、將組好的數據向下傳遞調用方法- (void)_queueMessage:(WVJBMessage*)message

- (void)_queueMessage:(WVJBMessage*)message {
     //self.startupMessageQueue這個是初始化的消息隊列,通常沒有自定義初始化消息隊列的話這個就是nil,直接就走到else裏面去了
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}
複製代碼

四、調用方法- (void)_dispatchMessage:(WVJBMessage*)message序列化消息,並在主線程中轉發 序列化後的樣板啊 {\"callbackId\":\"objc_cb_1\",\"data\":{\"greetingFromObjC\":\"Hi there, JS!\"},\"handlerName\":\"testJavascriptHandler\"} 而後會調用_evaluateJavascript方法,實質上這個地方是經過代理去調用不一樣的webview的執行js的方法 UIwebview會調用 - (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

WKWebView會調用 - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

五、此舉能夠用oc來調用js方法,此處就調到了js的方法WebViewJavascriptBridge._handleMessageFromObjC() 看看它的代碼啊

function _dispatchMessageFromObjC(messageJSON) {
		if (dispatchMessagesWithTimeoutSafety) {
			setTimeout(_doDispatchMessageFromObjC);
		} else {
			 _doDispatchMessageFromObjC();
		}
		
		function _doDispatchMessageFromObjC() {
		  //將json字符串轉換成json對象(能夠理解爲oc中的字典對象)
			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 });
					};
				}
				//查找js中註冊過的方法,若沒有js註冊此方法則報錯,反之取出儲存的該方法,並調用之
				var handler = messageHandlers[message.handlerName];
				if (!handler) {
					console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
				} else {
					handler(message.data, responseCallback);
				}
			}
		}
	}
複製代碼

此時執行方法_doDispatchMessageFromObjC時會走到else這一步,若是oc調的這個方法須要回調,則message.callbackId不會爲undefined,則js會調用_doSend方法回調oc,完成以後調用回調函數

六、上面方法完成最後一步是send方法了 先看看這個回調函數實現

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;
}
複製代碼

此處因爲自己就是oc調用js的回調,沒有再js調用oc後回調js,則responseCallback爲undefined,直接將其加入消息隊列中,並調用messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE來調用原生,這個調法感受有些奇怪,但從現象和個人理解來看就是給iframe加了一個src,相似於load了一個特殊的url 即https://__wvjb_queue_message__/

七、WKWebview的代理方法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler會攔截到這個url

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    //獲取js 「loadUrl」的url連接
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    if ([_base isWebViewJavascriptBridgeURL:url]) {//是否是WebViewJavascriptBridge約定的url
        if ([_base isBridgeLoadedURL:url]) {//是否是初始化指令__bridge_loaded__
            //注入核心js代碼
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {//是否是消息指令__wvjb_queue_message__
            //調用WebViewJavascriptBridgeBase的API去分發消息
            [self WKFlushMessageQueue];
        } else {//未知的url
            [_base logUnkownMessage:url];
        }
        //取消跳轉
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    //能走到這裏證實已經不是WebViewJavascriptBridge約定的url了,作正常跳轉
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}
複製代碼

這裏截取到的url是__wvjb_queue_message__則會調用方法- (void)WKFlushMessageQueue

八、分發消息

- (void)WKFlushMessageQueue {

//該方法首先會調用webViewJavascriptFetchQueyCommand 方法,這個方法是在js中 調用_fetchQueue()這個方法用來獲取queue中消息sendMessageQueue
webViewJavascriptFetchQueyCommand
[_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];
    }];
}
複製代碼

sendMessageQueue是一個在js中維護的消息隊列,是一個數組sendMessageQueue拿給oc而後將數據清空,在上面的這個oc函數evaluateJavaScript中result就是該js方法的返回值,即消息隊列[{"handlerName":"testJavascriptHandler","responseId":"objc_cb_4","responseData":{"Javascript Says":"Right back atcha!"}}] 九、查找js中維護的消息對,只有匹配上了才能調用上

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

oc一旦取到了js給返回的值,就會調用方法- (void)flushMessageQueue:(NSString *)messageQueueString

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

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

複製代碼

有responseId則證實是回調方法返回,而後就是在oc中的_responseCallbacks返回回調方法中找到該回調block,並回調相應的方法

這就是oc調用js的流程:大概總結以下 oc 告訴js我要發交互發消息了 ==> js 獲取到通知,並主動去「load」 __wvjb_queue_message__ 告訴oc把消息的內容傳過來

oc 得知js已經知道要傳遞消息了,主動調用js中的方法WebViewJavascriptBridge._handleMessageFromObjC()並在這個方法裏面將消息以字符串的形式傳過去 ==> js拿到消息內容後進行解析,在js上下文中保存的消息名中進行匹配,獲得js的調用方法,並調用該方法

JavaScript調用objective-c的方法

看完oc調用js的整個流程之後,再來看js調用oc的流程就明晰了不少,如今做以下講解:

一、js中的按鈕首先會觸發器onclick事件,而後調用bridge的方法callHandler,

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

二、callHandler在作了簡單的參數處理後轉而調用核心函數_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;
    }
複製代碼

_doSend方法負責組裝參數,並保存到上下文中,而後就「loadUrl」了

三、接下來就是wkwebview代理方式發光的時候了,- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler攔截到約定好的url https://__wvjb_queue_message__/

四、是時候調用一波原生方法- (void)WKFlushMessageQueue來獲取消息隊列了

五、調用方法- (void)flushMessageQueue:(NSString *)messageQueueString該方法中處理序列化的字符串變成數組,遍歷消息隊列,查找到oc中已經註冊好的對應的方法,匹配成功後調用該方法,則會調到註冊處的回調方法

完成相應處理,並回調其callback

六、此時處理回調的msg,包裝好後插入到oc須要處理的消息隊列

七、處理消息,將字典轉換成json字符串,調用方法WebViewJavascriptBridge._handleMessageFromObjCjs方法將oc的數據傳遞給js

八、緊接着就是js方法調用_handleMessageFromObjC() ==> _handleMessageFromObjC() ==> _doDispatchMessageFromObjC()

九、而後就是找到註冊過的回調方法,回調相關的函數

這即是js調用oc並獲取回調的流程。是否是以爲oc ==> js ==> oc 和 js==> oc ==> js 兩個流程很類似,能夠說是完美對稱了?這也就是開頭所說的對稱美啊!

WebViewJavascriptBridge初始化過程

html代碼裏面但是必備的哈,熟悉使用WebViewJavascriptBridge框架的痛惜應該是比較熟悉的了

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)
}
複製代碼

html已加載後就會一把它,它會「loadUrl」 https://__bridge_loaded__/load了這個後,

這時,咱們會injectJavascriptFile,將WebViewJavascriptBridge_JS.m中的js注入到web運行的上下文中,而後檢查startupMessageQueue,看有沒有初始化時候須要調用什麼方法(我理解應該是這樣的,方便自定義一些什麼初始化方法什麼的),默認這個是nil,也就不會執行下面的內容

注入js後,緊接着就是執行js腳本了,來斷點一波

咱們看到也就是一波初始化了,而後就是註冊方法_disableJavascriptAlertBoxSafetyTimeout這個東西,暫時木有用過啊

這就是我研讀WebViewJavascriptBridge框架源碼的筆記了,大神看了勿噴啊。之後還有我在公司項目中的關於wkwebview開發的一下心得,近期會總結一波,謝謝親的耐心閱讀啊,哪裏有問題的能夠私信我了~

相關文章
相關標籤/搜索