這兩天接近元旦,事情稍微少些,有些時間,索性寫點什麼,就從最擅長的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開發的一下心得,近期會總結一波,謝謝親的耐心閱讀啊,哪裡有問題的可以私信我了~