• objC与js通信实现--WebViewJavascriptBridge


    场景

      在移动端开发中,最为流行的开发模式就是hybmid开发,在这种native和h5的杂糅下,既能在某些需求中保证足够的性能,也可以在某些列表详情的需求下采用h5的样式控制来丰富内容。但是在大型产品的开发中,往往前端的职责不仅仅是h5的编写,还包括基本业务逻辑的实现,比如在h5页面中确定当前用户所在的城市(location),我们可以采用html5规范的Geolocation接口,但是更为通俗的做法是调用native的本地接口,因此这种常规的场景就涉及到了js和native层通信的问题,这在手淘开发中经常遇到,手淘提供了中间层windvane(jsBridge)来完成通信,不过由于windvane特殊性并未开源,因此本文着重分析WebViewJavascriptBridge框架(针对iOS)的通信机制。

    突破口

      iOS下h5页面承载在webView视图中,webView提供比较特殊的接口是stringByEvaluatingJavaScriptFromString方法,它让js字符串在当前的webview提供的js全局上下文中执行脚本,因此我们通过在objC层调用stringByEvaluatingJavaScriptFromString,执行h5下js得相关函数,以返回值的形式获取js端提供的相关调用函数数组并在webview下的上下文中执行函数数组,最终完成objC->js的通信(调用)。
      js调用objC则有些特殊,不过依然利用stringByEvaluatingJavaScriptFromString方法实现基本通信,并在objC层针对webviewDelegate接口提供的webView:shouldStartLoadWithRequest:navigationType方法进行捕获js层的调用。具体的方法是通过创建一个隐藏的iframe并对其src按照既定的schema格式赋值,并在objC层的webView:shouldStartLoadWithRequest:navigationType判断schema是否正确,如正确,则加载执行相关脚本,否则不执行。

    源码分析

    objC层调用js层注册函数:

       - (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];
       }
    

      参数data为传递给js层函数的参数,可为NSString、NSDictionary等对象,responseCallback则为objC层的回调,此回调函数执行流程简述为“js层注册函数执行完毕后,会返回带有responseId的消息,最后在objC层取出(回调存储在responseCallbacks字典中)对应的回调(即此处的responseCallback),并执行”,handlerName则为js层定义的函数名称。
      源码中在_queueMessage方法进行逻辑判断:若在h5页面或者js资源并未加载完毕时,在objC层webview中就调用了js函数,则会把相关的操作(存储为Message格式)存储在startupMessageQueue,等待相关资源加载完毕(即在webview的webViewDidFinishLoad生命周期函数中执行存储在startupMessageQueue的命令数组,执行完毕并清空该队列)再调用js层函数;否则若startupMessageQueue队列为空,则直接执行暴露在js端的webViewJavascriptBridge._handleMessageFromObjC函数,获取被调用的函数名和传入参数,以及在objC层sendData:responseCallback:handlerName中设置的回调函数id--callbackId,最终执行js层注册函数,并最终向objC层发送“_doSend({ responseId:callbackResponseId, responseData:responseData })”格式的消息,待objC接收到消息,解析responseId,执行回调函数。

        // 在objC端调用,用作ref
        function _dispatchMessageFromObjC(messageJSON) {
    	setTimeout(function _timeoutDispatchMessageFromObjC() {
    		var message = JSON.parse(messageJSON)
    		var messageHandler
    		var responseCallback
    
            // js调用objC成功后,objC返回带有responseId的消息,在js端执行回调
    	if (message.responseId) {
    		responseCallback = responseCallbacks[message.responseId]
    		if (!responseCallback) { return; }
    		responseCallback(message.responseData)
    		delete responseCallbacks[message.responseId]
    	} else {
                    // objC调用js后,构造返回给objC的消息
    		if (message.callbackId) {
    			var callbackResponseId = message.callbackId
    
                            // 执行回调成功后,选择性返回给objC
    			responseCallback = function(responseData) {
    				_doSend({ responseId:callbackResponseId, responseData:responseData })
    			}
    		}
    				
    		var handler = WebViewJavascriptBridge._messageHandler
    		if (message.handlerName) {
    			handler = messageHandlers[message.handlerName]
    		}
    				
    		try {
    			handler(message.data, responseCallback)
    		} catch(exception) {
    			if (typeof console != 'undefined') {
    				console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
    			}
    		}
    	   }
        })
    

      js触发objC调用是从_doSend函数开始的,主要就是通过给iframe设置schema完成:

        var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme'
        var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__'
        // js调用objC,通过对webview设置schema拦截
        // 拦截成功后,通过在objC端evaluateJs的_fetchQueue()来获取传入的参数
        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
        }
    

      由此可见,schema的格式为“wvjbscheme://WVJB_QUEUE_MESSAGE”,objC在webview的代理方法中webView:shouldStartLoadWithRequest:navigationType侦听schema格式,进而判断是否执行js的命令。

    js层调用objC层注册函数

      正如上节提到,在webView:shouldStartLoadWithRequest:navigationType中侦听schema格式,判断是否消息是否来自js层的函数调用:

        - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:   (UIWebViewNavigationType)navigationType {
          if (webView != _webView) { return YES; }
          NSURL *url = [request URL];
          __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
          if ([_base isCorrectProcotocolScheme:url]) {
              if ([_base isCorrectHost:url]) {
                  NSString *messageQueueString = [self _evaluateJavascript:@"WebViewJavascriptBridge._fetchQueue();"];
                  [_base flushMessageQueue:messageQueueString];
              } else {
                  [_base logUnkownMessage:url];
              }
              return NO;
          } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
              return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
          } else {
              return YES;
          }
        }
    

      若消息来自于js层,则在webview上下文执行WebViewJavascriptBridge._fetchQueue()函数,该函数的定义为

        function _fetchQueue() {
    	var messageQueueString = JSON.stringify(sendMessageQueue)
    	sendMessageQueue = []
    	return messageQueueString
        }
    

      _fetchQueue返回js端的调用命令队列messageQueueString,并在objC层执行flushMessageQueue:messageQueueString方法,将调用命令数组序列化,并执行objC层定义的函数,这个调用的过程类似上节中objC调用js层定义的函数,也是当objC层定义的函数执行完后,向js层触发消息,消息格式依然和上节“js向objC发送的消息格式一样,形如{ responseId:callbackResponseId, responseData:responseData }”,当js层接收到消息,执行js层的回调函数。
      因此综上来看,不管objC和js如何通信,最为关键的就是stringByEvaluatingJavaScriptFromString方法,它构建起了objC和js通信的基石,“objC可以直接通过该方法调用js函数,js也可让objC通过该方法获取js的调用队列,从而在objC层执行命令”。

    总结

      上文提到的仅仅是大体的通信机制,具体的实现细节仍有很多需要注意,比如如何在js端侦听通信组件的初始化事件、应该在何时在objC层调用js定义的函数、objC发送消息中序列化特殊字符等等,但是通信的机制可以通过本文略知一二。

  • 相关阅读:
    资源汇总
    最近面试经历
    Screen Space Ambient Occlusion
    实时渲染下的多线程协作:实践
    High Dynamic Range
    Deferred Shading
    实时渲染下的多线程协作
    9.了解四个相等判断的用法
    14.使用构造函数链
    16.垃圾最小化
  • 原文地址:https://www.cnblogs.com/accordion/p/4807538.html
Copyright © 2020-2023  润新知