前言
隨著業(yè)務(wù)的增多,我們的項(xiàng)目也集成了很多的h5頁面,隨之而來的就是h5和原生之間的交互。OC與javascript交互的那部分是在WebViewJavascriptBridge的github地址的基礎(chǔ)上修改的,WebViewJavascriptBridge應(yīng)該是當(dāng)前最流行最成功的OC與Web交互實(shí)現(xiàn)了。最近看了一下WebViewJavascriptBridge的實(shí)現(xiàn)原理,現(xiàn)在蘋果也已經(jīng)廢棄了UIWebview,因此只解析WKWebView的實(shí)現(xiàn)過程。
在不使用WebViewJavascriptBridge時(shí)我們可以在OC中調(diào)用javascript方法,但是反過來不能在javascript中調(diào)用OC方法。所以WebViewJavascriptBridge的實(shí)現(xiàn)過程就是在OC環(huán)境和javascript環(huán)境各自保存一個(gè)相互調(diào)用的信息。每一個(gè)調(diào)用之間都有id和callbackid來找到兩個(gè)環(huán)境對應(yīng)的處理。其實(shí)可以理解為原生和h5的生命周期是兩個(gè)不同的域。我們現(xiàn)在項(xiàng)目中大部分還是oc。因此舉例也都是使用oc和h5舉例。iOS有自己的生命周期,視圖展示,消失等等,這里可以理解為iOS的域,h5也有自己的生命周期,也可以理解為h5的域。兩者之間的通信就需要一個(gè)橋梁來進(jìn)行溝通。WebViewJavascriptBridge就是一個(gè)很好的橋梁溝通了h5和oc。
主要先介紹一下主要的文件作用。
WebViewJavascriptBridge_JS.m //文件中是javascript環(huán)境的bridge初始化和處理,里面負(fù)責(zé)接收oc發(fā)給javascript的消息,并且把javascript環(huán)境的消息發(fā)送給oc。
WKWebViewJavascriptBridge.m // 主要負(fù)責(zé)OC環(huán)境的消息處理,并且把OC環(huán)境的消息發(fā)送給javascript環(huán)境。
WebViewJavascriptBridgeBase.m //主要實(shí)現(xiàn)了OC環(huán)境的bridge初始化和處理。
各個(gè)初始化過程
1、oc的初始化
//初始化一個(gè)OC環(huán)境的橋WKWebViewJavascriptBridge并且初始化。
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
//調(diào)用下面那個(gè)方法
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
//初始化
- (void) _setupInstance:(WKWebView*)webView {
_webView = webView;
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
//messageHandlers用于保存OC環(huán)境注冊的方法,key是方法名,value是這個(gè)方法對應(yīng)的回調(diào)block
//startupMessageQueue用于保存是實(shí)話過程中需要發(fā)送給javascirpt環(huán)境的消息。
//responseCallbacks用于保存OC于javascript環(huán)境相互調(diào)用的回調(diào)模塊。通過_uniqueId加上時(shí)間戳來確定每個(gè)調(diào)用的回調(diào)。
- (id)init {
if (self = [super init]) {
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
return self;
}
oc宇javasecipt之間的交互都是通過messageHandlers和responseCallbacks來的,這個(gè)其實(shí)讓我想到最近流行的flutter也是通過三個(gè)handler來維持原生和dart之間的通信的。廢話不多說繼續(xù)聊一下WKWebViewJavascriptBridge。
2、OC環(huán)境注冊
注冊一個(gè)OC方法OC提供方法給JS調(diào)用,并且把他的回調(diào)實(shí)現(xiàn)保存在messageHandlers中。
[_bridge registerHandler:@"OC提供方法給JS調(diào)用" handler:^(id data, WVJBResponseCallback responseCallback) {
//NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"OC發(fā)給JS的返回值");
}];
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
3、JavaScript初始化
加載html,其實(shí)我覺得這個(gè)步驟寫在OC中和h5都可以。這個(gè)無非就是將WebViewJavascriptBridge注冊到JavaScript中,讓JavaScript中可以調(diào)用注冊的OC方法。如果是OC方法注入的話需要注意的就是在本地調(diào)用網(wǎng)頁加載完成后在進(jìn)行調(diào)用。
h5中的寫法
function setupWebViewJavascriptBridge(callback) {
//第一次調(diào)用這個(gè)方法的時(shí)候,為false
if (window.WebViewJavascriptBridge) {
var result = callback(WebViewJavascriptBridge);
return result;
}
//第一次調(diào)用的時(shí)候,也是false
if (window.WVJBCallbacks) {
var result = window.WVJBCallbacks.push(callback);
return result;
}
//把callback對象賦值給對象。
window.WVJBCallbacks = [callback];
//這段代碼的意思就是執(zhí)行加載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執(zhí)行的時(shí)候傳入的參數(shù),這是一個(gè)方法。
function callback(bridge) {
var uniqueId = 1
//把WEB中要注冊的方法注冊到bridge里面
bridge.registerHandler('OC調(diào)用JS提供的方法', function(data, responseCallback) {
log('OC調(diào)用JS方法成功', data)
var responseData = { 'JS給OC調(diào)用的回調(diào)':'回調(diào)值!' }
log('OC調(diào)用JS的返回值', responseData)
responseCallback(responseData)
})
};
//驅(qū)動所有hander的初始化
setupWebViewJavascriptBridge(callback);
// OC中的寫法
NSString *jsSetupBridge = @"window.JSBridge={};\
function setupWebViewJavascriptBridge(callback) {\
if (window.WebViewJavascriptBridge) {\
var result = callback(WebViewJavascriptBridge);\
return result;}\
if (window.WVJBCallbacks) {\
var result = window.WVJBCallbacks.push(callback);\
return result;}\
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);
}";
[self.wkWebView evaluateJavaScript:jsSetupBridge
completionHandler:^(id _Nullable response, NSError *_Nullable error){
}];
/**
* @brief 確保 JS 在頁面加載完成后調(diào)用
*/
[self.wkWebView
evaluateJavaScript:@"setupWebViewJavascriptBridge(null)"
completionHandler:^(id _Nullable response, NSError *_Nullable error) {
NSLog(js方法注入);
}];
我們調(diào)用setupWebViewJavascriptBridge函數(shù),并且這個(gè)函數(shù)傳入的callback也是一個(gè)函數(shù)。callback函數(shù)中有我們在javascript環(huán)境中注冊的OC調(diào)用JS提供的方法方法。setupWebViewJavascriptBridge的實(shí)現(xiàn)過程中我們可以發(fā)現(xiàn),如果不是第一次初始化,會通過window.WebViewJavascriptBridge或者window.WVJBCallbacks兩個(gè)判斷返回。
iframe可以理解為webview中的窗口,當(dāng)我們改變iframe的src屬性的時(shí)候,相當(dāng)于我們?yōu)g覽器實(shí)現(xiàn)了鏈接的跳轉(zhuǎn)。比如從www.baidu.com跳轉(zhuǎn)到www.google.com。下面這段代碼的目的就是實(shí)現(xiàn)一個(gè)到https://bridge_loaded的跳轉(zhuǎn)。從而達(dá)到初始化javascript環(huán)境的bridge的作用
//這段代碼的意思就是執(zhí)行加載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);
我們知道只要webview有跳轉(zhuǎn),就會調(diào)用webview的代理方法。我們重點(diǎn)看下面這個(gè)代理方法。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
NSLog(@"點(diǎn)開URL%@",url);
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
//如果是WebViewJavascriptBridge發(fā)送或者接受的消息,則特殊處理。否則按照正常流程處理。
if ([_base isWebViewJavascriptBridgeURL:url]) {
//1第一次注入JS代碼
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
//處理WEB發(fā)過來的消息
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
}
//下面是webview的正常代理執(zhí)行流程,不用管。
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
在這段代碼中,我們首先通過[_base isWebViewJavascriptBridgeURL:url]來判斷是否是普通的跳轉(zhuǎn)還是webViewjavascriptBridege的跳轉(zhuǎn)。如果是bridge_loaded表示是初始化javascript環(huán)境的消息,如果是wvjb_queue_message則表示是發(fā)送javascript消息。https://bridge_loaded顯然是第一種消息。OC具體具體判斷邏輯代碼如下:
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage @"__wvjb_queue_message__"
#define kBridgeLoaded @"__bridge_loaded__"
//是否是WebViewJavascriptBridge框架相關(guān)的鏈接
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url {
if (![self isSchemeMatch:url]) {
return NO;
}
BOOL result = [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url];
return result;
}
/*
是否是WebViewJavascriptBridge發(fā)送或者接受的消息
*/
- (BOOL)isSchemeMatch:(NSURL*)url {
NSString* scheme = url.scheme.lowercaseString;
BOOL result = [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
return result;
}
//是WebViewJavascriptBridge發(fā)送的消息還是WebViewJavascriptBridge的初始化消息。
- (BOOL)isQueueMessageURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}
//是否是https://__bridge_loaded__這種初始化加載消息
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
BOOL result = [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
return result;
}
接下來調(diào)用[_base injectJavascriptFile]方法,這個(gè)方法的作用就是把WebViewJavascriptBridge_JS.js中的方法注入到webview中并且執(zhí)行,從而達(dá)到初始化javascript環(huán)境的brige的作用。
//初始化的是否注入WebViewJavascriptBridge_JS.js
- (void)injectJavascriptFile {
NSString *js;
//WebViewJavascriptBridge_JS.js文件內(nèi)容其實(shí)就是WebViewJavascriptBridge_JS.m對應(yīng)的內(nèi)容,我只是把它整理方便閱讀。
if (true) {
js = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"WebViewJavascriptBridge_JS.js" ofType:nil] encoding:NSUTF8StringEncoding error:nil];
}else{
js = WebViewJavascriptBridge_js();
}
//把javascript代碼注入webview中執(zhí)行,這里執(zhí)行具體的注入操作。
[self _evaluateJavascript:js];
//如果javascript環(huán)境初始化完成以后,有startupMessageQueue消息。則立即發(fā)送消息。
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
//把javascript代碼寫入webview
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
return NULL;
}
4、WebViewJavascriptBridge_JS.m解析
上面我們講到了注入javascript方法到webview中。具體的代碼就是WebViewJavascriptBridge_JS.js這個(gè)文件中的方法。我們通過分析這個(gè)文件的代碼可以知道javascript環(huán)境的bridge是如何初始化的。
;(function() {
//如果已經(jīng)初始化了,則返回。
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
}
}
//初始化一些屬性。
var messagingIframe;
//用于存儲消息列表
var sendMessageQueue = [];
//用于存儲消息
var messageHandlers = {};
//通過下面兩個(gè)協(xié)議組合來確定是否是特定的消息,然后攔擊。
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
//oc調(diào)用js的回調(diào)
var responseCallbacks = {};
//消息對應(yīng)的id
var uniqueId = 1;
//是否設(shè)置消息超時(shí)
var dispatchMessagesWithTimeoutSafety = true;
//web端注冊一個(gè)消息方法
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
//web端調(diào)用一個(gè)OC注冊的消息
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;
}
//把消息轉(zhuǎn)換成JSON字符串返回
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
//OC調(diào)用JS的入口方法
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
//初始化橋接對象,OC可以通過WebViewJavascriptBridge來調(diào)用JS里面的各種方法。
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
//處理從OC返回的消息。
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
//回調(diào)
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {//主動調(diào)用
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
};
}
//獲取JS注冊的函數(shù)
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
//調(diào)用JS中的對應(yīng)函數(shù)處理
handler(message.data, responseCallback);
}
}
}
}
//把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作。
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
//存儲消息的回調(diào)ID
responseCallbacks[callbackId] = responseCallback;
//把消息對應(yīng)的回調(diào)ID和消息一起發(fā)送,以供消息返回以后使用。
message['callbackId'] = callbackId;
}
//把消息放入消息列表
sendMessageQueue.push(message);
//下面這句話會出發(fā)JS對OC的調(diào)用
//讓webview執(zhí)行跳轉(zhuǎn)操作,從而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
//messagingIframe.body.style.backgroundColor="#0000ff";
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
//注冊_disableJavascriptAlertBoxSafetyTimeout方法,讓OC可以關(guān)閉回調(diào)超時(shí),默認(rèn)是開啟的。
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
//執(zhí)行_callWVJBCallbacks方法
setTimeout(_callWVJBCallbacks, 0);
//初始化WEB中注冊的方法。這個(gè)方法會把WEB中的hander注冊到bridge中。
//下面的代碼其實(shí)就是執(zhí)行WEB中的callback函數(shù)。
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
})();
其實(shí)我們發(fā)現(xiàn)整個(gè)js文件就是一個(gè)立即執(zhí)行的javascript方法。
首先我們發(fā)現(xiàn)會初始化一個(gè)WebViewJavascriptBridge對象。并且這個(gè)對象是賦值給window對象,這里window對象可以理解為webview。所以說我們后面在OC環(huán)境中如果要調(diào)用js方法,就可以通過window.WebViewJavascriptBridge在加上具體方法來調(diào)用。
WebViewJavascriptBridge對象中有javascript環(huán)境注入的提供給OC調(diào)用的方法registerHandler,javascript調(diào)用OC環(huán)境方法的callHandler。
_fetchQueue這個(gè)方法的作用就是把javascript環(huán)境的方法序列化成JSON字符串,然后傳入OC環(huán)境再轉(zhuǎn)換。
_handleMessageFromObjC就是處理OC發(fā)給javascript環(huán)境的方法。
在這個(gè)文件中也初始化了一個(gè)iframe實(shí)現(xiàn)webview的url跳轉(zhuǎn)功能,從而激發(fā)webview代理方法的調(diào)用。
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
//messagingIframe.body.style.backgroundColor="#0000ff";
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
上面的src就是https://wvjb_queue_message/。這個(gè)是javascript發(fā)送的OC的第一條消息,目的和上面OC環(huán)境的startupMessageQueue一樣,就是在javascript環(huán)境初始化完成以后,把javascript要發(fā)送給OC的消息立即發(fā)送出去。
然后我們看文件的最后面有如下代碼。這段代碼的作用就是立即執(zhí)行html中的callback方法。callback中傳入的bridge參數(shù)就是我們這里初始化的window.WebViewJavascriptBridge對象。
//執(zhí)行_callWVJBCallbacks方法
setTimeout(_callWVJBCallbacks, 0);
//初始化WEB中注冊的方法。這個(gè)方法會把WEB中的hander注冊到bridge中。
//下面的代碼其實(shí)就是執(zhí)行WEB中的callback函數(shù)。
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
直到這里,OC環(huán)境和javascript環(huán)境的bridege都建立完畢。OC和javascript環(huán)境都有一個(gè)bridge對象,這個(gè)對象都保存著注冊的每個(gè)方法和回調(diào),并且維護(hù)著各自的消息隊(duì)列、回調(diào)id、requestId等一系列信息。
OC發(fā)消息給WEB
OC要調(diào)用javascript環(huán)境的方法,其實(shí)就是調(diào)用html中的bridge.registerHandler注冊的方法。
//點(diǎn)擊按鈕開始一個(gè)OC消息.ExampleWKWebViewController.m中一個(gè)方法開始。
- (void)callHandler:(id)sender {
id data = @{ @"OC調(diào)用JS方法": @"OC調(diào)用JS方法的參數(shù)" };
[_bridge callHandler:@"OC調(diào)用JS提供的方法" data:data responseCallback:^(id response) {
// NSLog(@"testJavascriptHandler responded: %@", response);
}];
}
/*
handerName:OC調(diào)用JS提供的方法
data:{@"OC調(diào)用JS方法的參數(shù)":@"OC調(diào)用JS方法"}
responseCallback:回調(diào)block
*/
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
把所有信息存入一個(gè)名字為message的字典中。里面拼裝好參數(shù)data、回調(diào)IDcallbackId、消息名字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];
}
把OC消息序列化、并且轉(zhuǎn)化為javascript環(huán)境的格式。然后在主線程中調(diào)用_evaluateJavascript。
//把消息發(fā)送給WEB環(huán)境
- (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];
});
}
}
具體注入的javascript字符串如下:
WebViewJavascriptBridge._handleMessageFromObjC('{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC調(diào)用JS方法\":\"OC調(diào)用JS方法的參數(shù)\"},\"handlerName\":\"OC調(diào)用JS提供的方法\"}');
其實(shí)就是通過javascript環(huán)境中的Bridge對象的_handleMessageFromObjC方法。下面我們?nèi)ebViewJavascriptBridege_JS.js中看_handleMessageFromObjC的處理過程
//處理從OC返回的消息。
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
//回調(diào)
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {//主動調(diào)用
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
};
}
//獲取JS注冊的函數(shù)
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
//調(diào)用JS中的對應(yīng)函數(shù)處理
handler(message.data, responseCallback);
}
}
}
}
上面這段代碼很容易理解,其實(shí)就是如果消息中有callbackId則表示是一個(gè)回調(diào)。直接調(diào)用_doSend方法把信息返回OC。否則就是Web環(huán)境主動調(diào)用OC的情況。此時(shí)把callbackID、handlerName、responseCallback封裝進(jìn)一個(gè)message對象中保存起來(其實(shí)你會發(fā)現(xiàn)和OC環(huán)境的bridge處理一樣)。然后通過_doSend發(fā)消息發(fā)送到OC環(huán)境。下面我們看看_doSend的具體實(shí)現(xiàn):
//把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作。
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
//存儲消息的回調(diào)ID
responseCallbacks[callbackId] = responseCallback;
//把消息對應(yīng)的回調(diào)ID和消息一起發(fā)送,以供消息返回以后使用。
message['callbackId'] = callbackId;
}
//把消息放入消息列表
sendMessageQueue.push(message);
//下面這句話會出發(fā)JS對OC的調(diào)用
//讓webview執(zhí)行跳轉(zhuǎn)操作,從而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
其中最重要還是最后面的通過改變iframe的messagingIframe.src。從而觸發(fā)webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler從而在OC中處理javascript環(huán)境觸發(fā)過來的回調(diào)。具體如下:
if ([_base isWebViewJavascriptBridgeURL:url]) {
//第一次注入JS代碼
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
//處理WEB發(fā)過來的消息
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
}
這里會走[self WKFlushMessageQueue];方法。然后通過調(diào)用WebViewJavascriptBridge._fetchQueue()來獲取javascript給OC的回調(diào)信息
//獲取WEB消息的JSON字符串
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
////把消息或者WEB回調(diào)從OC發(fā)送到OC
- (void)WKFlushMessageQueue {
NSString *js = [_base webViewJavascriptFetchQueyCommand];
[_webView evaluateJavaScript:js completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
//把消息或者WEB回調(diào)從OC發(fā)送到OC
[_base flushMessageQueue:result];
}];
}
獲取到j(luò)avascript給OC的回調(diào)消息以后,然后把javascript的bridge返回的信息加工處理成OC環(huán)境的bridge能識別的信息。從而找到具體的實(shí)現(xiàn)執(zhí)行。
//把從WEB發(fā)送的消息返回。然后在這里處理
- (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);
}
}
}
這里會調(diào)用handler方法,通過javascript傳過來的responseId獲取對應(yīng)的WVJBResponseCallback。然后執(zhí)行這個(gè)block。到這里從OC發(fā)送消息到j(luò)avascript并且javascript返回消息給OC的流程走完了。
WEB發(fā)消息給OC
首先通過html中的bridge.callHandler方法,這里的bridge就是window.WebViewJavascriptBridge對象:
bridge.callHandler('OC提供方法給JS調(diào)用',params, function(response) {
log('JS調(diào)用OC的返回值', response)
})
接下來調(diào)用window.WebViewJavascriptBridge中的callHander方法
//web端調(diào)用一個(gè)OC注冊的消息
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName: handlerName, data: data }, responseCallback);
}
然后調(diào)用WebViewJavascriptBridge_JS.js中的方法執(zhí)行具體的操作。具體就和OC調(diào)用javascript過程一樣了,就不解釋了。
//把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作。
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
//存儲消息的回調(diào)ID
responseCallbacks[callbackId] = responseCallback;
//把消息對應(yīng)的回調(diào)ID和消息一起發(fā)送,以供消息返回以后使用。
message['callbackId'] = callbackId;
}
//把消息放入消息列表
sendMessageQueue.push(message);
//下面這句話會出發(fā)JS對OC的調(diào)用
//讓webview執(zhí)行跳轉(zhuǎn)操作,從而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
總結(jié)
其實(shí)現(xiàn)在想想,大致的原理如下。
分別在OC域和javascript域都保存一個(gè)bridge對象,里面維持著requestId,callbackId,以及每個(gè)id對應(yīng)的具體實(shí)現(xiàn)。
OC通過javascript域的window.WebViewJavascriptBridge對象來找到具體的方法,然后執(zhí)行。
javascript通過改變iframe的src來出發(fā)webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler從而實(shí)現(xiàn)把javascript消息發(fā)送給OC這個(gè)功能。
兩個(gè)域都有自己生命周期,oc的開始的生命周期先于h5開始的生命周期。
其實(shí)這里只是解析了webview與OC交互的橋接問題,其實(shí)還有很多的其他的問題,比如進(jìn)度條、請求攔截等。就不在一一擴(kuò)展了
參考文文章:
http://www.itdecent.cn/p/d45ce14278c7
https://www.cnblogs.com/LeeGof/p/8143408.html
https://blog.csdn.net/zuggs_/article/details/80649669