上一篇博文重點講了下我們項目中最常用的JS調(diào)用OC, 花開兩朵各表一枝, 本文將重點講下OC調(diào)用JS.
OC調(diào)用JS的入口在VC, 下面是代碼
[self.bridge callHandler:@"getUserInfo" data:@{@"userId":@"DX001"} responseCallback:^(id responseData) {
NSString *userInfo = [NSString stringWithFormat:@"%@,姓名:%@,年齡:%@", responseData[@"userID"], responseData[@"userName"], responseData[@"age"]];
UIAlertController *vc = [UIAlertController alertControllerWithTitle:@"從網(wǎng)頁端獲取的用戶信息" message:userInfo preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:nil];
[vc addAction:cancelAction];
[vc addAction:okAction];
[self presentViewController:vc animated:YES completion:nil];
}];
WebViewJavascriptBridge.m
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
WebViewJavascriptBridgeBase.m
- (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];
}
最終來到了WebViewJavascriptBridgeBase
的sendData
方法里面, 這里創(chuàng)建一個NSMutableDictionary
對象message
, 并把VC傳遞進(jìn)來的參數(shù)data = @{@"userId":@"DX001"}
, handlerName = @"getUserInfo"
還有responseCallback
保存起來, 和之前JS保存responseCallback
方法相似, 這里也是生成一個callbackId
, 并把responseCallback
保存在以callbackId
為key的字典self.responseCallbacks
中, 最后執(zhí)行[self _queueMessage:message];
WebViewJavascriptBridgeBase.h
@interface WebViewJavascriptBridgeBase : NSObject
@property (strong, nonatomic) NSMutableArray* startupMessageQueue;
WebViewJavascriptBridgeBase.m
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
我們來回憶下上文是怎么使用_dispatchMessage
的
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
這里是在OC的block中執(zhí)行了_queueMessage
, 實際也是OC調(diào)用JS. 只是在上文中, OC調(diào)用JS不是重點. 好了這里也順便分析下我們之前遺留下來的問題:startupMessageQueue
是干什么的?
WebViewJavascriptBridgeBase.m
-(id)init {
self = [super init];
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
return(self);
}
- (void)dealloc {
self.startupMessageQueue = nil;
self.responseCallbacks = nil;
self.messageHandlers = nil;
}
- (void)reset {
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
- (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];
}
}
}
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
startupMessageQueue
是WebViewJavascriptBridgeBase
中的一個數(shù)組, 這個數(shù)組在WebViewJavascriptBridgeBase
初始化的時候被創(chuàng)建, 但是只是一個空的數(shù)組, 并且在初始化注入的時候就被取出來并置空了, 所以后面正常情況是不存在_queueMessage
走進(jìn)if分支的, 只有一種情況, 就是在injectJavascriptFile
還沒執(zhí)行的時候, 先進(jìn)行了OC對JS的調(diào)用, 這種情況在我們把
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 = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
這段代碼寫進(jìn)html的時候應(yīng)該是不存在的, 因為在網(wǎng)頁加載的時候OC就完成了注入, 但是如果上面的這段代碼如果不在html中, 那還是有可能的, 而且在實際開發(fā)中, 難道我們還要要求前端的同事每個網(wǎng)頁都加上面的一段代碼, 也是不現(xiàn)實的. 所以作者應(yīng)該是通過綜合的考慮才加入了startupMessageQueue
的, 好了, 在本例中, startupMessageQueue
還是沒有實際作用, 代碼最終回到上文的后半部分, OC回調(diào)JS, 這里要注意一下的問題就是, 運行JS腳本可能會存在線程安全的問題, 所以, 一定要在主線程執(zhí)行JS
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
經(jīng)過一些列調(diào)用_queueMessage->_dispatchMessage->_evaluateJavascript->_handleMessageFromObjC->_dispatchMessageFromObjC->_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);
}
}
}
我們在Safari中下斷點, 發(fā)現(xiàn)
, 這里有個小的tips, 因為這段腳本不在html的頁面里面不能直接打斷點, 要先在
bridge.registerHandler('getUserInfo', function(data, responseCallback) {
console.log("OC中傳遞過來的參數(shù):", data);
// 把處理好的結(jié)果返回給OC
responseCallback({"userID":"DX001", "userName":"旋之華", "age":"18", "otherName":"旋之華"})
});
responseCallback
這里打斷點, 然后調(diào)用JS接口, Safari左側(cè)會出現(xiàn)調(diào)用堆棧, 里面有我們注入的代碼, 這時候就可以在_dispatchMessageFromObjC
里面打斷點了, 好了, 繼續(xù)分析_dispatchMessageFromObjC
里面的代碼.
由于調(diào)用的時候并沒有給responseId
賦值, 所以, 代碼走到
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);
}
這里是和之前JS調(diào)用OC時候, OC回調(diào)JS不同的, 這里message.responseId
是沒有值的, message.callbackId
是有值的, 所以會在這里創(chuàng)建一個JS的responseCallback
, 后面取出handler
并調(diào)用handler(message.data, responseCallback);
, 下面來看下messageHandlers
吧, 實際和OC注冊很相似
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
還沒完, 這里OC調(diào)用JS的時候也傳遞了一個block, 這個block最終傳遞到了JS
bridge.registerHandler('getUserInfo', function(data, responseCallback) {
console.log("OC中傳遞過來的參數(shù):", data);
// 把處理好的結(jié)果返回給OC
responseCallback({"userID":"DX001", "userName":"旋之華", "age":"18", "otherName":"旋之華"})
});
把函數(shù)名作為key, 回調(diào)方法作為value, 建立messageHandlers
字典, 所以最終執(zhí)行handler(message.data, responseCallback);
實際是調(diào)用了
bridge.registerHandler('getUserInfo', function(data, responseCallback) {
console.log("OC中傳遞過來的參數(shù):", data);
// 把處理好的結(jié)果返回給OC
responseCallback({"userID":"DX001", "userName":"旋之華", "age":"18", "otherName":"旋之華"})
});
中函數(shù)體里的代碼, 如果沒有OC傳遞的block, OC調(diào)用JS就到此結(jié)束了, 輸出console.log("OC中傳遞過來的參數(shù):", data);
完成調(diào)用, 但是OC傳遞了block, 所以還要繼續(xù)分析, responseCallback
是在剛才通過JS代碼創(chuàng)建的回調(diào), 只有OC傳遞了block才會創(chuàng)建.
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
在JS中調(diào)用responseCallback({"userID":"DX001", "userName":"旋之華", "age":"18", "otherName":"旋之華"})
實際會來到_doSend
, responseData
正是JS中傳遞來的參數(shù)
handlerName
和responseId
都是OC調(diào)用的時候傳遞的參數(shù).
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;
}
進(jìn)入_doSend
, 由于沒傳遞responseCallback
, 所以if走不到, 這里還是把OC傳遞過來的message
保存在sendMessageQueue
中, 然后改變src
觸發(fā)OC執(zhí)行, 來到OC的
- (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
有值, 而從_responseCallbacks
取出來的responseCallback正是OC之前傳入的block, 所以下面的代碼
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
執(zhí)行responseCallback
, 回調(diào)OC之前傳遞的block, 至此, OC調(diào)用JS, 并把JS端數(shù)據(jù)取回完美結(jié)束.
總結(jié)
在實際中, 我們應(yīng)該很少會用到JS提供接口給OC調(diào)用, 通常是OC提供穩(wěn)定通用接口給JS調(diào)用, 所以本文不是我們實踐的重點, 但是作為講解框架的完整性, 我們應(yīng)該把OC調(diào)用JS和JS調(diào)用OC都進(jìn)行詳細(xì)的分析, 這樣能更好的理解作者設(shè)計的意圖和架構(gòu)的巧妙之處.
遺留問題
1 難道每個Web頁面都要加入setupWebViewJavascriptBridge
這段代碼, 這應(yīng)該是所有開發(fā)者都不能接受的.
2 對于WKWebView怎么處理? 也能攔截嗎?
3 如何設(shè)計一個通用的WebView或者WebViewController?
下面是本文用到的代碼的github, 不是小編原著.
WebViewJSBridgeDemo