uiwebView與OC交互

WebViewJavaScriptBridge是IOS中JS和OC交互的常用框架,支持以下兩種調用:

1. OC端的方法如下

Method Frome OC

Method 1 是注冊一個OC的方法--testObjcCallback,handler是JS掉用的內容,responseCallback是將OC處理返回給JS的回調(對應的是上述第2種調用);

Method 2 是調用JS的方法的testJavascriptHandler方法,@{ @"foo":@"before ready" }是需要傳遞的參數,responseCallback是將JS處理結果返回給OC的回調(對應的是上述的第1種調用)

2. JS端的方法如下

Method Frome JS

Method 1 是JS注冊一個方法供OC調用,responseCallback(responseData)是將處理結果返回OC。

Method 2 是在點擊了一個按鈕之后JS調用OC的方法,{'foo': 'bar'}是給OC的參數,response是OC處理后返回給JS的數據。

注:JS中是可以不寫;號的,這和swift一樣

JS調用OC,OC將處理結果回調給JS:要想被JS調用,我們首先要注冊一個handler,和回調的? ? ? ? ? ? block,注冊時候以鍵值對的形式存儲這個block,handler,當JS調用OC時調用webView:shouldStartLoadWithRequest:navigationType:這個方法,根據JS傳來的數據,找到之前保存的Block并且調用,同時新建一個需要把處理結果回調給JS的Blcok,OC處理完結果之后調用剛才創建的Block利用stringByEvaluatingJavaScriptFromString將處理結果返回給JS。

OC調用JS時與此類似。基于這個流程,我們來看WebViewJavaScriptBridge的實現過程。

原理

接下來我們來分析從頁面加載到OC和JS互相調用的整個過程:

一、準備工作

當加載HTML文件的時候調用[webView loadHTMLString:appHtml baseURL:baseURL];,這時會調用:

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {if(webView != _webView) {returnYES; }NSURL*url = [request URL];? ? __strongWVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;if([_base isWebViewJavascriptBridgeURL:url]) {if([_base isBridgeLoadedURL:url]) {? ? ? ? ? ? [_base injectJavascriptFile];? ? ? ? }elseif([_base isQueueMessageURL:url]) {NSString*messageQueueString = [self_evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];? ? ? ? ? ? [_base flushMessageQueue:messageQueueString];? ? ? ? }else{? ? ? ? ? ? [_base logUnkownMessage:url];? ? ? ? }returnNO;? ? }elseif(strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {return[strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];? ? }else{returnYES;? ? }}


在這個方法中判斷URL的類型,如果是WebViewJavascriptBridgeURL那么就會判斷是BridgeLoadedURL,QueueMessageURL還是未知的URL,在首次調用時是返回YES的,然后的URL就是BridgeLoadedURL,我們在看它的判斷條件[self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];Scheme是自己設置的https,那么BridgeLoaded(__bridge_loaded__)是什么呢?我們看ExampleApp.html文件,發現它的script標簽中有這么一段代碼:

functionsetupWebViewJavascriptBridge(callback){if(window.WebViewJavascriptBridge) {returncallback(WebViewJavascriptBridge); }if(window.WVJBCallbacks) {returnwindow.WVJBCallbacks.push(callback); }window.WVJBCallbacks = [callback];varWVJBIframe =document.createElement('iframe');? ? WVJBIframe.style.display ='none';? ? WVJBIframe.src ='https://__bridge_loaded__';document.documentElement.appendChild(WVJBIframe);? ? setTimeout(function(){document.documentElement.removeChild(WVJBIframe) },0)}setupWebViewJavascriptBridge(function(bridge){varuniqueId =1functionlog(message, data){varlog =document.getElementById('log')varel =document.createElement('div')? ? ? ? el.className ='logLine'el.innerHTML = uniqueId++ +'. '+ message +':
'+JSON.stringify(data)if(log.children.length) { log.insertBefore(el, log.children[0]) }else{ log.appendChild(el) }? ? }


在這里我們發現了https://__bridge_loaded__這個iframe的src,并且在接下來調用setupWebViewJavascriptBridge時這個src會當做一個請求,這時會調用shouldStartLoadWithRequest這個方法。此時就滿足了isBridgeLoadedURL這個請求。這時就會調用

[_base injectJavascriptFile]

注入一個JS文件,這個JS文件的主要內容是(篇幅問題,有刪減):

window.WebViewJavascriptBridge = {registerHandler: registerHandler,callHandler: callHandler,disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,_fetchQueue: _fetchQueue,_handleMessageFromObjC: _handleMessageFromObjC};varmessagingIframe;varsendMessageQueue = [];varmessageHandlers = {};varCUSTOM_PROTOCOL_SCHEME ='https';varQUEUE_HAS_MESSAGE ='__wvjb_queue_message__';varresponseCallbacks = {};varuniqueId =1;vardispatchMessagesWithTimeoutSafety =true;functionregisterHandler(handlerName, handler){? ? messageHandlers[handlerName] = handler;}functioncallHandler(handlerName, data, responseCallback){? ? _doSend();}functiondisableJavscriptAlertBoxSafetyTimeout(){? ? dispatchMessagesWithTimeoutSafety =false;}function_doSend(message, responseCallback){? ? sendMessageQueue.push(message);? ? messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE;}function_fetchQueue(){varmessageQueueString =JSON.stringify(sendMessageQueue);? ? sendMessageQueue = [];returnmessageQueueString;}function_dispatchMessageFromObjC(messageJSON){if(dispatchMessagesWithTimeoutSafety) {? ? ? ? setTimeout(_doDispatchMessageFromObjC);? ? }else{? ? ? ? _doDispatchMessageFromObjC();? ? }function_doDispatchMessageFromObjC(){? ? ? ? }? ? }}function_handleMessageFromObjC(messageJSON){? ? _dispatchMessageFromObjC(messageJSON);}messagingIframe =document.createElement('iframe');messagingIframe.style.display ='none';messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE;document.documentElement.appendChild(messagingIframe);registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);setTimeout(_callWVJBCallbacks,0);function_callWVJBCallbacks(){varcallbacks =window.WVJBCallbacks;deletewindow.WVJBCallbacks;for(vari=0; i


下面我們來分析下注入的JavaScript的內容。

給window對象添加一個屬性WebViewJavascriptBridge(JS中可以直接給對象添加屬性),這個對象包含以下內容:

? 1) registerHandler:注冊調用方法

? 2)callHandler:調用OC時的方法

? 3)disableJavscriptAlertBoxSafetyTimeout:超時時彈框是否展示的標示

? 4)_fetchQueue:獲取Queue對象的方法

? 5)_handleMessageFromObjC:處理OC調用的方法

2.定義了一系列的變量來存儲數據

messagingIframe:iframe標簽,當我們的WebView加載它的時候,會調用其中的src,src就是調用請求的URL。

1)sendMessageQueue:message數組? 2)messageHandlers:handler對象 *JS中{}表示對象*? 3)CUSTOM_PROTOCOL_SCHEME:scheme標示? 4)QUEUE_HAS_MESSAGE:有Message標識? 5)responseCallbacks:回調對象? 6)uniqueId:唯一標示ID

進過系列一的剖析,我們明白了使用WebViewJavaScriptBridge前需要做的準備工作,那么接下來,我們一起探討OC和JS相互調用的具體執行過程以及其中的要點。

二、 JS調用OC,然后OC將處理結果返回JS

1. OC首先注冊JS將調用的方法

OC調用registerHandler:,這時將其調用信息存儲在messageHandlers字典中以handlerName為Key,給JS處理結果的Block為Value(_base.messageHandlers[handlerName] = [handler copy]);

2. 在JS中調用被注冊的方法

JS調用

bridge.callHandler('testObjcCallback', {'foo':'bar'},function(response){? ? ? ? ? ? log('JS got response', response)? ? ? ? })

來調用上文OC注冊的方法,這個brige就是上文注入JS代碼時候創建的,我們再它內部做了什么。

functioncallHandler(handlerName, data, responseCallback){if(arguments.length ==2&&typeofdata =='function') {? ? ? ? responseCallback = data;? ? ? ? data =null;? ? }? ? _doSend({handlerName:handlerName,data:data }, responseCallback);}

這里判斷了參數類型,如果傳入的參數只有兩個,并且第二個是function類型,那么就將第二個參數變為callBack,data置空,將handlerName和data轉化成一個對象的兩個屬性并傳給_doSend()。

function_doSend(message, responseCallback){if(responseCallback) {varcallbackId ='cb_'+(uniqueId++)+'_'+newDate().getTime();? ? ? ? ? ? ? ? responseCallbacks[callbackId] = responseCallback;? ? ? ? ? ? ? ? message['callbackId'] = callbackId;? ? ? ? ? ? }? ? ? ? ? ? sendMessageQueue.push(message);? ? ? ? ? ? messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE;? ? ? ? }


這里的responseCallback是JS先調用OC然后OC調用JS時才會有的,如果這種情況,那么需要用唯一的標識(callbackId),來將這個responseCallback存儲在responseCallbacks中,并且給message添加callbackId這個屬性。這個數值會在下次OC調用JS的時候作為唯一的Key被用到。軟后將message放入:sendMessageQueue隊列中,然后拼接src。

3. 在回掉方法中攔截相應的方法,然后調用block.

經過方法步驟2,會調用下面的回調方法

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType{}

在這個方法中調用

NSString*messageQueueString = [self_evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];? [_base flushMessageQueue:messageQueueString];

首先獲取JS中的messageQueue(步驟2中的sendMessageQueue),然后調用flushMessageQueue:方法:

idmessages = [self_deserializeMessageJSON:messageQueueString];for(WVJBMessage* messageinmessages) {if(![message isKindOfClass:[WVJBMessageclass]]) {NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messageclass], message);continue;? ? }? ? [self_log:@"RCVD"json:message];/////////*********OC先調用了JS,JS再調用了OC*********///////////NSString* responseId = message[@"responseId"];if(responseId) {//調用之前存儲的BolckWVJBResponseCallback responseCallback = _responseCallbacks[responseId];? ? ? ? responseCallback(message[@"responseData"]);? ? ? ? [self.responseCallbacks removeObjectForKey:responseId];/////////*********JS先調用OC,OC再調用JS*********////////////// 這里是JS先調用OC的時候存儲的是 JS的回調函數}else{// JS先調用的OC,OC再調用JSWVJBResponseCallback responseCallback =NULL;NSString* callbackId = message[@"callbackId"];if(callbackId) {? ? ? ? ? ? responseCallback = ^(idresponseData) {if(responseData ==nil) {? ? ? ? ? ? ? ? ? ? responseData = [NSNullnull];? ? ? ? ? ? ? ? }//JS調用OC時候的存儲(后續OC調用JS返回計算結果)WVJBMessage* msg = @{@"responseId":callbackId,@"responseData":responseData };? ? ? ? ? ? ? ? [self_queueMessage:msg];? ? ? ? ? ? };? ? ? ? }else{? ? ? ? ? ? responseCallback = ^(idignoreResponseData) {// Do nothing};? ? ? ? }? ? ? ? ? ? ? ? WVJBHandler handler =self.messageHandlers[message[@"handlerName"]];if(!handler) {NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);continue;? ? ? ? }//調用OC的Block,同時,如果OC調用responseCallback,則調用_queueMessage進行相應的處理handler(message[@"data"], responseCallback);? ? }}


這里先將返回的JSON字符串轉換成對象,這里的字符串是調用

function_fetchQueue(){varmessageQueueString =JSON.stringify(sendMessageQueue);? ? sendMessageQueue = [];returnmessageQueueString;? }

獲取的,這里將sendMessageQueue轉為JSON,然后將其置空,這里為啥使用數組而不用對象來存儲呢?因為可能JS還沒有處理結束就有兩次調用,要保證他們不丟失使用了數組。然后判斷數組中的Message對象是否有responseId(JS調用OC第一次時存儲的),這里沒有responseId所以走else:如果有callbackId(在JS中作為回調用的),定義responseCallback,這個block就是OC將處理結果返回給JS時用到的block。如果沒有callbackId說明,不需要回調JS,這個時候responseCallback為空。最后調用步驟1中存儲在messageHandlers對象中的block,并且將剛才創建的responseCallback作為參數傳入,以便OC將計算結果傳遞給JS。

4. OC將計算結果返回給JS

[_bridge registerHandler:@"testObjcCallback"handler:^(iddata, WVJBResponseCallback responseCallback) {NSLog(@"testObjcCallback called: %@", data);? ? responseCallback(@"response form oc's call back");? }];

在handler的最后一步調用responseCallback()將處理結果回調給JS。這個responseCallback()就是我們在步驟3中創建的responseCallback。我們再來看這個block。看步驟3可以看到這個其內部調用

[self_queueMessage:msg];? [self_dispatchMessage:message];

在_dispatchMessage內部執行:

NSString* javascriptCommand = [NSStringstringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];

接下來JS中的_handleMessageFromObjC就會接收到OC傳過來處理結果。

function_doDispatchMessageFromObjC(){varmessage =JSON.parse(messageJSON);varmessageHandler;varresponseCallback;if(message.responseId) {? ? ? ? ? ? responseCallback = responseCallbacks[message.responseId];if(!responseCallback) {return;? ? ? ? ? ? }? ? ? ? ? ? responseCallback(message.responseData);deleteresponseCallbacks[message.responseId];? ? ? ? }else{// OC先調用JS是用到}? ? }

這個時候我們看到了步驟三中的responseId的作用了,這時候responseId就表明了是OC將處理結果傳遞給JS并不需要JS再調用OC了,這時只調用responseCallback(message.responseData);將數據傳給JS。

這樣我們就完成了JS調用OC,然后OC將結果回調給JS的全部過程。

三、OC調用JS,然后JS將處理結果返回給OC

1. JS注冊相應的方法供回調

同OC注冊方法時候一樣,JS也是用一個messageHandlers對象來存儲

functionregisterHandler(handlerName, handler){? ? messageHandlers[handlerName] = handler;? ? }

2. OC調用JS時存儲調用信息

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {NSMutableDictionary* message = [NSMutableDictionarydictionary];if(data) {? ? ? ? ? ? message[@"data"] = data;? ? ? ? }if(responseCallback) {NSString* callbackId = [NSStringstringWithFormat:@"objc_cb_%ld", ++_uniqueId];self.responseCallbacks[callbackId] = [responseCallbackcopy];? ? ? ? ? ? message[@"callbackId"] = callbackId;? ? ? ? }if(handlerName) {? ? ? ? ? ? message[@"handlerName"] = handlerName;? ? ? ? }? ? ? ? [self_queueMessage:message];? ? }

這里使用message字典來存儲參數,方法名,使用responseCallbacks來存儲JS處理完之后,需要回調的Block(這里為了確保多次調用不會覆蓋之前的調用,使用了唯一的callbackId)。

同上文所述,最終會調用

- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {return[_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];? ? }

3. JS調用_dispatchMessageFromObjC

這時message沒有responseId,會走else,

if(message.callbackId) {varcallbackResponseId = message.callbackId;? ? ? ? ? ? ? ? ? ? responseCallback =function(responseData){? ? ? ? ? ? ? ? ? ? ? ? _doSend({handlerName:message.handlerName,responseId:callbackResponseId,responseData:responseData });? ? ? ? ? ? ? ? ? ? };? ? ? ? ? ? ? ? }varhandler = messageHandlers[message.handlerName];if(!handler) {console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);? ? ? ? ? ? ? ? }else{? ? ? ? ? ? ? ? ? ? handler(message.data, responseCallback);? ? ? ? ? ? ? ? } 這里定義了需要給OC傳遞結果的`responseCallback`,取出之前注冊的`handler`:`messageHandlers[message.handlerName]`,然后調用這個`handler`,并將這個`responseCallback`作為參數傳進去,`handler(message.data, responseCallback);`

4. JS將結果回傳給OC

在步驟三中調用handler:function(data, responseCallback){? ? ? ? ? ? log('ObjC called testJavascriptHandler with', data)varresponseData = {'Javascript Says':'Right back atcha!'}? ? ? ? ? ? log('JS responding with', responseData)? ? ? ? ? ? responseCallback(responseData)? ? ? ? } 在這個`handler`的結尾調用步驟三種的`responseCallback`(傳入的只有數據沒有回調),根據步驟三可以看出來其會調用`_doSend`方法。該方法中由于沒有傳進去回調,所以不會給message對象添加`callbackId`,只調用? ? ? ? sendMessageQueue.push(message);? ? ? ? ? messagingIframe.src = CUSTOM_PROTOCOL_SCHEME +'://'+ QUEUE_HAS_MESSAGE;

這是由于含有responseId(在步驟三中的_doSend調用時設置),所以只會取出之前存儲的block,并且將結果回傳給OC:

//調用之前存儲的BolckWVJBResponseCallback responseCallback = _responseCallbacks[responseId];? ? ? ? responseCallback(message[@"responseData"]);? ? ? ? [self.responseCallbacks removeObjectForKey:responseId];

至此,OC和JS交互的所有邏輯已介紹完畢(WKWebView實現方式相同),總結下兩種情景的回調,其實現方式及其相似,正如文章開頭的總結。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379