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实现方式相同),总结下两种情景的回调,其实现方式及其相似,正如文章开头的总结。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。