天天看點

iOS WebviewJavascriptBridge 源碼研讀筆記

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

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

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

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

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

WebViewJavascriptBridge VS WKWebViewJavascriptBridge

這個架構還是有點666啊,既支援iOS又支援mac OS 但鑒于我們mac OS 用的少,就直接看iOS部分了

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

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

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

對比

WebViewJavascriptBridge

WKWebViewJavascriptBridge

兩個類的頭檔案,看處WKWebViewJavascriptBridge多了一個

reset

方法,其他的方法兩個類幾乎一毛一樣,我們繼續看.m實作檔案也證明了這一點,差别僅在于webview的實作,這也印證了這個架構的核心隻是WebViewJavascriptBridgeBase,其核心都是通過js中去“loadUrl”(這個是本人自己習慣這麼說,友善了解啊,實際上和loadUrl有點差别,不過道理是一樣的)然後webview在代理方法中去攔截特殊約定好的url,然後進行消息的處理。

以下是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部分的實作過程

1、原生按鈕回調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);
    }];
}
複制代碼
           

2、調用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;
}
複制代碼
           

3、将組好的資料向下傳遞調用方法

- (void)_queueMessage:(WVJBMessage*)message

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

4、調用方法

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

5、此舉可以用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,完成之後調用回調函數

6、上面方法完成最後一步是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__/

7、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

8、分發消息

- (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!"}}]

9、查找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的流程就明晰了很多,現在作如下講解:

1、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);
    }
複制代碼
           

2、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”了

3、接下來就是wkwebview代理方式發光的時候了,

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler

攔截到約定好的url

https://__wvjb_queue_message__/

4、是時候調用一波原生方法

- (void)WKFlushMessageQueue

來擷取消息隊列了

5、調用方法

- (void)flushMessageQueue:(NSString *)messageQueueString

該方法中處理序列化的字元串變成數組,周遊消息隊列,查找到oc中已經注冊好的對應的方法,比對成功後調用該方法,則會調到注冊處的回調方法

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

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

7、處理消息,将字典轉換成json字元串,調用方法

WebViewJavascriptBridge._handleMessageFromObjC

js方法将oc的資料傳遞給js

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

9、然後就是找到注冊過的回調方法,回調相關的函數

這便是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開發的一下心得,近期會總結一波,謝謝親的耐心閱讀啊,哪裡有問題的可以私信我了~

轉載于:https://juejin.im/post/5a45eb105188251fbd33f148

繼續閱讀