iOS開發(fā)WebViewJavascriptBridge通訊原理解析
前言
H5頁面具有跨平臺、開發(fā)容易、上線不需要跟隨App的版本等優(yōu)點(diǎn),但H5頁面也有體驗(yàn)不如native好、沒有native穩(wěn)定等問題。所以目前大部分App都是使用Hybrid混合開發(fā)的。
當(dāng)然有了H5頁面就少不了H5與native交互,交互就會用到bridge的能力了。
WebViewJavascriptBridge是一個native與JS進(jìn)行消息互通的第三方庫,本章會簡單解析一下WebViewJavascriptBridge的源碼和實(shí)現(xiàn)原理。
通訊原理
JavaScriptCore
JavaScriptCore作為iOS的JS引擎為原生編程語言O(shè)C、Swift 提供調(diào)用 JS 程序的動態(tài)能力,還能為 JS 提供原生能力來彌補(bǔ)前端所缺能力。 iOS中與JS通訊使用的是JavaScriptCore庫,正是因?yàn)镴avaScriptCore這種起到的橋梁作用,所以也出現(xiàn)了很多使用JavaScriptCore開發(fā)App的框架,比如RN、Weex、小程序、Webview Hybrid等框架。 如圖:
當(dāng)然JS引擎不光有蘋果的JavaScriptCore,谷歌有V8引擎、Mozilla有SpiderMoney
JavaScriptCore本章只簡單介紹,后面主要解析WebViewJavascriptBridge。因?yàn)閡iwebview已經(jīng)不再使用了,所以后面提到的webview都是wkwebview,demo也是以wkwebview進(jìn)行解析。
源碼解析
代碼結(jié)構(gòu)
除了引擎層外,還需要native、h5和WebViewJavascriptBridge三層才能完成一整個信息通路。WebViewJavascriptBridge就是中間那個負(fù)責(zé)通信的SDK。
WebViewJavascriptBridge的核心類主要包含幾個:
- WebViewJavascriptBridge_JS:是一個JS的字符串,作用是JS環(huán)境的Bridge初始化和處理。負(fù)責(zé)接收native發(fā)給JS的消息,并且把JS環(huán)境的消息發(fā)送給native。
- WKWebViewJavascriptBridge/WebViewJavascriptBridge:主要負(fù)責(zé)WKWebView和UIWebView相關(guān)環(huán)境的處理,并且把native環(huán)境的消息發(fā)送給JS環(huán)境。
- WebViewJavascriptBridgeBase:主要實(shí)現(xiàn)了native環(huán)境的Bridge初始化和處理。
初始化
WebViewJavascriptBridge是如何完成初始化的呢,首先要有webview容器,所以要對webview容器進(jìn)行初始化,設(shè)置代理,初始化WebViewJavascriptBridge對象,加載URL。
WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds]; webView.navigationDelegate = self; [self.view addSubview:webView]; // 開啟打印 [WebViewJavascriptBridge enableLogging]; // 創(chuàng)建bridge對象 _bridge = [WebViewJavascriptBridge bridgeForWebView:webView]; // 設(shè)置代理 [_bridge setWebViewDelegate:self];
這里加載的就是JSBridgeDemoApp這個本地的html文件。
NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"JSBridgeDemoApp" ofType:@"html"]; NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil]; NSURL *baseURL = [NSURL fileURLWithPath:htmlPath]; [webView loadHTMLString:appHtml baseURL:baseURL];
再看一下JSBridgeDemoApp這個html文件。
function setupWebViewJavascriptBridge(callback) { // 第一次調(diào)用這個方法的時候,為false if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); } // 第一次調(diào)用的時候,為false if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); } // 把callback對象賦值給對象 window.WVJBCallbacks = [callback]; // 加載WebViewJavascriptBridge_JS中的代碼 // 相當(dāng)于實(shí)現(xiàn)了一個到https://__bridge_loaded__的跳轉(zhuǎn) 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) } // 驅(qū)動所有hander的初始化 setupWebViewJavascriptBridge(function(bridge) { ... }
在JSBridgeDemoApp的script標(biāo)簽下,聲明了一個名為setupWebViewJavascriptBridge的方法,在加載html后直接進(jìn)行了調(diào)用。 setupWebViewJavascriptBridge方法中最核心的代碼是:
創(chuàng)建一個iframe標(biāo)簽,然后加載了鏈接為 https://bridge_loaded 的內(nèi)容。相當(dāng)于在當(dāng)前頁面內(nèi)容實(shí)現(xiàn)了一個到 https://bridge_loaded 的內(nèi)部跳轉(zhuǎn)。 ps:iframe標(biāo)簽用于在網(wǎng)頁內(nèi)顯示網(wǎng)頁,也使用iframe作為鏈接的目標(biāo)。
html文件內(nèi)部實(shí)現(xiàn)了這個跳轉(zhuǎn)后native端是如何監(jiān)聽的呢,在webview的代理里有一個方法:decidePolicyForNavigationAction 這個代理方法的作用是只要有webview跳轉(zhuǎn),就會調(diào)用到這個方法。代碼如下:
// 只要webview有跳轉(zhuǎn),就會調(diào)用webview的這個代理方法 - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { if (webView != _webView) { return; } NSURL *url = navigationAction.request.URL; __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate; // 如果是WebViewJavascriptBridge發(fā)送或者接收消息,則特殊處理。否則按照正常流程處理 if ([_base isWebViewJavascriptBridgeURL:url]) { if ([_base isBridgeLoadedURL:url]) { // 是否是 https://__bridge_loaded__ 這種初始化加載消息 [_base injectJavascriptFile]; } else if ([_base isQueueMessageURL:url]) { // https://__wvjb_queue_message__ // 處理WEB發(fā)過來的消息 [self WKFlushMessageQueue]; } else { [_base logUnkownMessage:url]; } decisionHandler(WKNavigationActionPolicyCancel); return; } // webview的正常代理執(zhí)行流程 ...
從上面的代碼中可以看到,如果監(jiān)聽的webview跳轉(zhuǎn)不是WebViewJavascriptBridge發(fā)送或者接收消息就正常執(zhí)行流程,如果是WebViewJavascriptBridge發(fā)送或者接收消息則對此攔截不跳轉(zhuǎn),并且針對消息進(jìn)行處理。 當(dāng)消息url是https://bridge_loaded 的時候,會去注入WebViewJavascriptBridge_js到JS中:
// 將WebViewJavascriptBrige_JS中的方法注入到webview中并且執(zhí)行 - (void)injectJavascriptFile { NSString *js = WebViewJavascriptBridge_js(); // 把javascript代碼注入webview中執(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]; } } }
[self _evaluateJavascript:js];就是執(zhí)行webview中的evaluateJavaScript:方法。把JS寫入webview。所以執(zhí)行完此處代碼JS當(dāng)中就有bridge這個對象了。初始化完成。
總結(jié):在加載h5頁面后會調(diào)用setupWebViewJavascriptBridge方法,該方法內(nèi)創(chuàng)建了一個iframe加載內(nèi)容為 https://bridge_loaded ,該消息被decidePolicyForNavigationAction監(jiān)聽到,然后執(zhí)行injectJavascriptFile去讀取WebViewJavascriptBridge_js將WebViewJavascriptBridge對象注入到當(dāng)前h5中。
WebViewJavascriptBridge 對象
整個WebViewJavascriptBridge_js文件其實(shí)就是一個字符串形式的js代碼,里面包含WebViewJavascriptBridge和相關(guān)bridge調(diào)用的方法。
// 初始化Bridge對象,OC可以通過WebViewJavascriptBridge來調(diào)用JS里面的各種方法 window.WebViewJavascriptBridge = { registerHandler: registerHandler, // JS中注冊方法 callHandler: callHandler, // JS中調(diào)用OC的方法 disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, _fetchQueue: _fetchQueue, // 把消息轉(zhuǎn)換成JSON串 _handleMessageFromObjC: _handleMessageFromObjC // OC調(diào)用JS的入口方法 };
WebViewJavascriptBridge對象里核心的方法有:
- registerHandler:JS中注冊方法
- callHandler: JS中調(diào)用native的方法
- _fetchQueue: 把消息轉(zhuǎn)換成JSON字符串
- _handleMessageFromObjC:native調(diào)用JS的入口方法
當(dāng)初始化完成后,WebViewJavascriptBridge對象和對象里的方法就已經(jīng)存在并且可用了。
JS和native是如何相互傳遞消息的呢?從上面的代碼中可以看到如果JS想要發(fā)送消息給native就會調(diào)用callHandler方法;如果native想要調(diào)用JS方法那JS側(cè)就必須先注冊一個registerHandler方法。
相對應(yīng)的我們看一下native側(cè)是如何與JS傳遞消息的,其實(shí)接口標(biāo)準(zhǔn)是一致的,native調(diào)JS的方法使用callHandler方法:
id data = @{ @"dataFromOC": @"aaaa!" }; [_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) { NSLog(@"JS回調(diào)的數(shù)據(jù)是:%@", response); }];
JS調(diào)native方法在native側(cè)就必須先注冊一個registerHandler方法:
// 注冊事件(h5調(diào)App) [_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) { NSLog(@"JSTOOCCallback called: %@", data); responseCallback(@"Response from JSTOOCCallback"); }];
也就是說native像JS發(fā)送消息的話,JS側(cè)要先注冊該方法registerHandler,native側(cè)調(diào)用callHandler; JS像native發(fā)送消息的話,native側(cè)要先注冊registerHandler,JS側(cè)調(diào)用callHandler。這樣才能完成雙端通信。
如圖:
native向JS發(fā)送消息
現(xiàn)在要從native側(cè)向JS側(cè)發(fā)送一條消息,方法名為:"OCToJSHandler",并且拿到JS的回調(diào),具體實(shí)現(xiàn)細(xì)節(jié)如下:
JS側(cè)
native向JS發(fā)送數(shù)據(jù),首先要在JS側(cè)去注冊這個方法:
bridge.registerHandler('OCToJSHandler', function(data, responseCallback) { ... })
這個registerHandler的實(shí)現(xiàn)在WebViewJavascriptBridge_JS是:
// web端注冊一個消息方法,將注冊的方法存儲起來 function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; }
就是將這個注冊的方法存儲到messageHandlers這個map中,key為方法名稱,value為function(data, responseCallback) {}這個方法。
native側(cè)
native側(cè)調(diào)用bridge的callHandler方法,傳參為data和一個callback回調(diào)
id data = @{ @"dataFromOC": @"aaaa!" }; [_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) { NSLog(@"JS回調(diào)的數(shù)據(jù)是:%@", response); }];
接下來會走到WebViewJavascriptBridgeBase的-sendData: responseCallback: handlerName:方法,該方法中將"data"和"handlerName"存入到一個message字典中,如果存在callback會生成一個callbackId一并存入到message字典中,并且將該回調(diào)存入到responseCallbacks中,key為callbackId,value為這個callback。代碼如下:
// 所有信息存入字典 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];
將message存儲到隊(duì)列等待執(zhí)行,執(zhí)行該條message時會先將message進(jìn)行序列化,序列化完成后將message拼接到字符串WebViewJavascriptBridge._handleMessageFromObjC('%@');中,然后執(zhí)行_evaluateJavascript執(zhí)行該js方法。
// 把OC消息序列化、并且轉(zhuǎn)化為JS環(huán)境的格式,然后在主線程中調(diào)用_evaluateJavascript - (void)_dispatchMessage:(WVJBMessage*)message { NSString *messageJSON = [self _serializeMessage:message pretty:NO]; NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON]; [self _evaluateJavascript:javascriptCommand]; }
_handleMessageFromObjC方法會將messageJSON傳遞給_dispatchMessageFromObjC進(jìn)行處理。 首先將messageJSON進(jìn)行解析,根據(jù)handlerName取出存儲在messageHandlers中的方法。如果該message中存在callbackId,將callbackId作為參數(shù)生成一個回調(diào)放到responseCallback中。 代碼如下:
function _doDispatchMessageFromObjC() { // 解析發(fā)送過來的JSON var message = JSON.parse(messageJSON); var messageHandler; var responseCallback; // 主動調(diào)用 // 如果有callbackid if (message.callbackId) { // 將callbackid當(dāng)做callbackResponseId再返回回去 var callbackResponseId = message.callbackId; responseCallback = function(responseData) { // 把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作 _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData }); }; // 獲取JS注冊的函數(shù),取出消息里的handlerName var handler = messageHandlers[message.handlerName]; // 調(diào)用JS中的對應(yīng)函數(shù)處理 handler(message.data, responseCallback); } }
handler方法其實(shí)就是名為"OCToJSHandler"的方法,這時就走到了registerHandler里的那個function(data, responseCallback) {}方法了。我們看一下方法內(nèi)部的具體實(shí)現(xiàn):
bridge.registerHandler('OCToJSHandler', function(data, responseCallback) { // OC中傳過來的數(shù)據(jù) log('從OC傳過來的數(shù)據(jù)是:', data) // JS返回?cái)?shù)據(jù) var responseData = { 'dataFromJS':'bbbb!' } responseCallback(responseData) })
data就是從native傳過來的數(shù)據(jù),responseCallback就是保存的回調(diào),然后又生成了新數(shù)據(jù)作為參數(shù)給到了這個回調(diào)。
responseCallback的實(shí)現(xiàn)是:
responseCallback = function(responseData) { // 把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作 _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData }); };
將該方法的handlerName、生成的callbackResponseId(也就是callbackId)以及JS返回的數(shù)據(jù)一起給到_doSend方法。
_doSend方法將message存儲到sendMessageQueue消息列表中,并使用messagingIframe加載了一次https://wvjb_queue_message。
// 把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作 function _doSend(message, responseCallback) { // 把消息放入消息列表 sendMessageQueue.push(message); // 發(fā)出js對oc的調(diào)用,讓webview執(zhí)行跳轉(zhuǎn)操作,可以在decidePolicyForNavigationAction:中攔截到j(luò)s發(fā)給oc的消息 messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
這時webview的監(jiān)聽方法decidePolicyForNavigationAction監(jiān)聽到了https://wvjb_queue_message 消息后還是執(zhí)行WebViewJavascriptBridge._fetchQueue()去取數(shù)據(jù),取到數(shù)據(jù)后根據(jù)responseId當(dāng)初在_responseCallbacks中存儲的callback,然后執(zhí)行callback、移除responseCallbacks中的數(shù)據(jù)。到此為止,整個native向JS發(fā)送消息的過程就完成了。
總結(jié):
- JS中先調(diào)用registerHandler將方法存儲到messageHandlers中
- native調(diào)用callHandler:方法,將消息內(nèi)容存儲到message中,回調(diào)存儲到responseCallbacks中。
- 將message消息序列化通過_evaluateJavascript方法執(zhí)行_handleMessageFromObjC
- 將message解析,通過message.handlerName從messageHandlers取出該方法;根據(jù)message.callbackId生成回調(diào)
- 執(zhí)行該方法,回調(diào)
JS向native發(fā)送消息
從JS向native發(fā)消息其實(shí)和native向JS發(fā)消息的接口層面是差不多的。
native側(cè)
native側(cè)首先要注冊一個JSTOOCCallback方法
[_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) { responseCallback(@"Response from JSTOOCCallback"); }];
該方法也同樣是將該方法的callback存儲起來,存儲到messageHandlers當(dāng)中,key就是方法名"JSTOOCCallback",value就是callback。
JS側(cè)
JS側(cè)會調(diào)用callHandler方法:
// 調(diào)用oc中注冊的那個方法 bridge.callHandler('JSTOOCCallback', {'foo': 'bar'}, function(response) { log('JS 取到的回調(diào)是:', response) })
這個callHandler方法同樣會調(diào)用_doSend方法:將callback存儲到responseCallbacks中,key為callbakid;將消息存儲到sendMessageQueue中;messagingIframe執(zhí)行https://wvjb_queue_message
native的decidePolicyForNavigationAction方法監(jiān)聽到該消息后同樣通過WebViewJavascriptBridge._fetchQueue()去取消息。
根據(jù)callbackId創(chuàng)建一個responseCallback,根據(jù)message的handlerName從messageHandlers取出該回調(diào),然后執(zhí)行:
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"]]; handler(message[@"data"], responseCallback);
調(diào)用完這個方法后,該消息已經(jīng)收到,然后將回調(diào)的內(nèi)容回調(diào)給JS。 通過上面的代碼可以看到,回調(diào)JS的內(nèi)容就是callbackId和responseData生成的message,調(diào)用_queueMessage方法。
_queueMessage方法上面已經(jīng)看過了,就是序列化消息、加入隊(duì)列、執(zhí)行WebViewJavascriptBridge._handleMessageFromObjC('%@');方法。
JS收到該消息后,處理返回的消息,從responseCallbacks中根據(jù)message中的responseId取出callback并且執(zhí)行。最后刪除responseCallbacks中的數(shù)據(jù),JS向native發(fā)送數(shù)據(jù)就完成了。
小結(jié):
- native側(cè)調(diào)用registerHandler方法注冊方法,方法名為JSTOOCCallback,將消息存儲到messageHandlers中,key為方法名,value為callback。
- JS側(cè)調(diào)用callHandler方法:將responseCallback存儲到responseCallbacks中;將message存儲到sendMessageQueue中;messagingIframe執(zhí)行 http://wvjb_queue_message
- native側(cè)監(jiān)聽到該消息后調(diào)用WebViewJavascriptBridge._fetchQueue()去取數(shù)據(jù)
- 根據(jù)handlerName從messageHandlers中取出該callback;根據(jù)callbackId創(chuàng)建callback對象作為參數(shù)放到handlerName的方法中;執(zhí)行該回調(diào)。
總結(jié)
綜上,WebViewJavascriptBridge的核心流程就分析完了,最核心的點(diǎn)是JS通過加載iframe來通知native側(cè);native側(cè)通過evaluateJavaScript方法去執(zhí)行JS。
從整個SDK來看,設(shè)計(jì)的非常好,值得借鑒學(xué)習(xí):
- 使用外觀模式統(tǒng)一調(diào)用接口,比如初始化WebViewJavascriptBridge的時候,不需要關(guān)心使用方使用的是UIWebView還是WKWebView,內(nèi)部已經(jīng)處理好了。
- 接口統(tǒng)一,不管是native側(cè)還是JS側(cè),調(diào)用方法就是callHandler、注冊方法就是registerHandler,不需要關(guān)注內(nèi)部實(shí)現(xiàn),使用非常方便。
- 代碼簡潔,邏輯清晰,層次分明。從類的分布就能很清晰的看出各自的功能是什么。
- 職責(zé)單一,比如decidePolicyForNavigationAction方法只負(fù)責(zé)監(jiān)聽事件、_fetchQueue是負(fù)責(zé)把消息轉(zhuǎn)換成JSON字符串返回、_doSend是發(fā)送消息到native、_dispatchMessageFromObjC是負(fù)責(zé)處理從OC返回的消息等。雖然decidePolicyForNavigationAction也能接收消息,但這樣就不會這么精簡了。
- 擴(kuò)展性好,目前decidePolicyForNavigationAction雖然只有初始化和發(fā)消息兩個事件,如果有其他事件還可以再擴(kuò)展,這也得益于方法設(shè)計(jì)的職責(zé)單一,擴(kuò)展對原有方法影響會很小。
以上就是iOS開發(fā)WebViewJavascriptBridge通訊原理解析的詳細(xì)內(nèi)容,更多關(guān)于iOS WebViewJavascriptBridge通訊的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IOS AFNetworking的Post失敗及requestSerializer的正確使用
這篇文章主要介紹了IOS AFNetworking的Post失敗及requestSerializer的正確使用的相關(guān)資料,需要的朋友可以參考下2017-05-05iOS實(shí)現(xiàn)轉(zhuǎn)場動畫的3種方法示例
這篇文章主要給大家介紹了關(guān)于iOS實(shí)現(xiàn)轉(zhuǎn)場動畫的3種方法,文中通過示例代碼以及圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03iOS UIBezierPath實(shí)現(xiàn)餅狀圖
這篇文章主要為大家詳細(xì)介紹了iOS UIBezierPath實(shí)現(xiàn)餅狀圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-03-03IOS 播放系統(tǒng)提示音使用總結(jié)(AudioToolbox)
這篇文章主要介紹了IOS 播放系統(tǒng)提示音使用總結(jié)(AudioToolbox)的相關(guān)資料,需要的朋友可以參考下2017-05-05