求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
优秀开源代码解读之JS与iOS Native Code互调的优雅实现方案
 

作者:yanghua_kobe ,发布于2012-12-6 ,来源:CSDN

 

简介

本篇为大家介绍一个优秀的开源小项目:WebViewJavascriptBridge。

它优雅地实现了在使用UIWebView时JS与ios 的ObjC nativecode之间的互调,支持消息发送、接收、消息处理器的注册与调用以及设置消息处理的回调。

就像项目的名称一样,它是连接UIWebView和Javascript的bridge。在加入这个项目之后,他们之间的交互处理方式变得很友好。

在native code中跟UIWebView中的js交互的时候,像下面这样:

//发送一条消息给UI端并定义回调处理逻辑
 [_bridge send:@"A string sent from ObjC before Webview has loaded." responseCallback:^(id error, id responseData) {
        if (error) { NSLog(@"Uh oh - I got an error: %@", error); }
        NSLog(@"objc got response! %@ %@", error, responseData);
 }];

而在UIWebView中的js跟native code交互的时候也变得很简洁,比如在调用处理器的时候,就可以定义回调处理逻辑:

//调用名为testObjcCallback的native端处理器,并传递参数,同时设置回调处理逻辑
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
<span style="white-space: pre;">	</span>log('Got response from testObjcCallback', response)
})

一起来看看它的实现吧,它总共就包含了三个文件:

WebViewJavascriptBridge.h
WebViewJavascriptBridge.m
WebViewJavascriptBridge.js.txt

它们是以如下的模式进行交互的:

很明显:WebViewJavascriptBridge.js.txt主要用于衔接UIWebView中的web page,而WebViewJavascriptBridge.h/m则主要用于与ObjC的native code打交道。他们作为一个整体,其实起到了一个“桥梁”的作用,这三个文件封装了他们具体的交互处理方式,只开放出一些对外的涉及到业务处理的API,因此你在需要UIWebView与Native code交互的时候,引入该库,则无需考虑太多的交互上的问题。整个的Bridge对你来说都是透明的,你感觉编程的时候,就像是web编程的前端和后端一样清晰。

简单地罗列一下它可以实现哪些功能吧:

出于表达上的需要,对于UIWebView相关的我就称之为UI端,而objc那端的处理代码称之为Native端。

【1】UI端

(1) UI端在初始化时支持设置消息的默认处理器(这里的消息指的是从Native端接收到的消息)

(2) 从UI端向Native端发送消息,并支持对于Native端响应后的回调处理的定义

(3) UI端调用Native定义的处理器,并支持Native端响应后的回调处理定义

(4) UI端注册处理器(供Native端调用),并支持给Native端响应处理逻辑的定义

【2】 Native端

(1) Native端在初始化时支持设置消息的默认处理器(这里的消息指的是从UI端发送过来的消息)

(2) 从Native端向UI端发送消息,并支持对于UI端响应后的回调处理逻辑的定义

(3) Native端调用UI端定义的处理器,并支持UI端给出响应后在Native端的回调处理逻辑的定义

(4) Native端注册处理器(供UI端调用),并支持给UI端响应处理逻辑的定义

UI端以及Native端完全是对等的两端,实现也是对等的。一段是消息的发送端,另一段就是接收端。这里为引起混淆,需要解释一下我这里使用的“响应”、“回调”在这个上下文中的定义:

(1) 响应:接收端给予发送端的应答

(2) 回调:发送端收到接收端的应答之后在接收端调用的处理逻辑

下面来分析一下源码:

WebViewJavascriptBridge.js.txt:

主要完成了如下工作:

(1) 创建了一个用于发送消息的iFrame(通过创建一个隐藏的ifrmae,并设置它的URL 来发出一个请求,从而触发UIWebView的shouldStartLoadWithRequest回调协议)

(2) 创建了一个核心对象WebViewJavascriptBridge,并给它定义了几个方法,这些方法大部分是公开的API方法

(3) 创建了一个事件:WebViewJavascriptBridgeReady,并dispatch(触发)了它。

代码解读

UI端实现

对于(1),相应的代码如下:

/*
 *创建一个iFrame,设置隐藏并加入到DOM中
 */
	function _createQueueReadyIframe(doc) {
		messagingIframe = doc.createElement('iframe')
		messagingIframe.style.display = 'none'
		doc.documentElement.appendChild(messagingIframe)
	} 

对于(2)中的WebViewJavascriptBridge,其对象拥有如下方法:

window.WebViewJavascriptBridge = {
		init: init,
		send: send,
		registerHandler: registerHandler,
		callHandler: callHandler,
		_fetchQueue: _fetchQueue,
		_handleMessageFromObjC: _handleMessageFromObjC
	}

方法的实现:

<span style="white-space: pre;">	</span>/*
	 *初始化方法,注入默认的消息处理器
	 *默认的消息处理器用于在处理来自objc的消息时,如果该消息没有设置处理器,则采用默认处理器处理
	 */
	function init(messageHandler) {
		if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') }
		WebViewJavascriptBridge._messageHandler = messageHandler
		var receivedMessages = receiveMessageQueue
		receiveMessageQueue = null
		//如果接收队列有消息,则处理
		for (var i=0; i<receivedMessages.length; i++) {
			_dispatchMessageFromObjC(receivedMessages[i])
		}
	}

<span style="white-space: pre;">	</span>/*
	 *发送消息并设置回调
	 */
	function send(data, responseCallback) {
		_doSend({ data:data }, responseCallback)
	}
	
	/*
	 *注册消息处理器
	 */
	function registerHandler(handlerName, handler) {
		messageHandlers[handlerName] = handler
	}
	
	/*
	 *调用处理器并设置回调
	 */
	function callHandler(handlerName, data, responseCallback) {
		_doSend({ data:data, handlerName:handlerName }, responseCallback)
	} 

涉及到的两个内部方法:

<span style="white-space: pre;">	</span>/*
 *内部方法:消息的发送
 */
 function _doSend(message, responseCallback) {
  //如果定义了回调
  if (responseCallback) {
   //为回调对象产生唯一标识
   var callbackId = 'js_cb_'+(uniqueId++)
   //并存储到一个集合对象里
   responseCallbacks[callbackId] = responseCallback
   //新增一个key-value对- 'callbackId':callbackId
   message['callbackId'] = callbackId
  }
  sendMessageQueue.push(JSON.stringify(message))
  messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
 }

<span style="white-space: pre;">	</span>/*
 *内部方法:处理来自objc的消息
 */
 function _dispatchMessageFromObjC(messageJSON) {
  setTimeout(function _timeoutDispatchMessageFromObjC() {
   var message = JSON.parse(messageJSON)
   var messageHandler
   if (message.responseId) {
    //取出回调函数对象并执行
    var responseCallback = responseCallbacks[message.responseId]
    responseCallback(message.error, message.responseData)
    delete responseCallbacks[message.responseId]
   } else {
    var response
    if (message.callbackId) {
     var callbackResponseId = message.callbackId
     response = {
      respondWith: function(responseData) {
       _doSend({ responseId:callbackResponseId, responseData:responseData })
      },
      respondWithError: function(error) {
       _doSend({ responseId:callbackResponseId, error:error })
      }
     }
    }
    var handler = WebViewJavascriptBridge._messageHandler
    //如果消息中已包含消息处理器,则使用该处理器;否则使用默认处理器
    if (message.handlerName) {
     handler = messageHandlers[message.handlerName]
    }
    try {
     handler(message.data, response)
    } catch(exception) {
     console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
    }
   }
  })
 } 

还有两个js方法是供native端直接调用的方法(它们本身也是为native端服务的):

<span style="white-space: pre;">	</span>/*
	 *获得队列,将队列中的每个元素用分隔符分隔之后连成一个字符串【native端调用】
	 */
	function _fetchQueue() {
		var messageQueueString = sendMessageQueue.join(MESSAGE_SEPARATOR)
		sendMessageQueue = []
		return messageQueueString
	}

<span style="white-space: pre;">	</span>/*
	 *处理来自ObjC的消息【native端调用】
	 */
	function _handleMessageFromObjC(messageJSON) {
		//如果接收队列对象存在则入队该消息,否则直接处理
		if (receiveMessageQueue) {
			receiveMessageQueue.push(messageJSON)
		} else {
			_dispatchMessageFromObjC(messageJSON)
		}
	} 

最后还有一段代码就是,定义一个事件并触发,同时设置设置上面定义的WebViewJavascriptBridge对象为事件的一个属性:

<span style="white-space: pre;">	</span>var doc = document
	_createQueueReadyIframe(doc)
	//创建并实例化一个事件对象
	var readyEvent = doc.createEvent('Events')
	readyEvent.initEvent('WebViewJavascriptBridgeReady')
	readyEvent.bridge = WebViewJavascriptBridge
	//触发事件
	doc.dispatchEvent(readyEvent) 

Native端实现

其实大致跟上面的类似,只是因为语法不同(所以我上面才说两端是对等的):

WebViewJavascriptBridge.h/.m

它其实可以看作UIWebView的Controller,实现了UIWebViewDelegate协议:

@interface WebViewJavascriptBridge : NSObject <UIWebViewDelegate>
+ (id)bridgeForWebView:(UIWebView*)webView handler:(WVJBHandler)handler;
+ (id)bridgeForWebView:(UIWebView*)webView webViewDelegate:(id <UIWebViewDelegate>)webViewDelegate handler:
(WVJBHandler)handler;
+ (void)enableLogging;
- (void)send:(id)message;
- (void)send:(id)message responseCallback:(WVJBResponseCallback)responseCallback;
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
@end 

方法的实现其实是跟前面类似的,这里我们只看一下UIWebView的一个协议方法
shouldStartLoadWithRequest:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:
(UIWebViewNavigationType)navigationType {
    if (webView != _webView) { return YES; }
    NSURL *url = [request URL];
    if ([[url scheme] isEqualToString:CUSTOM_PROTOCOL_SCHEME]) {
		//队列中有数据
        if ([[url host] isEqualToString:QUEUE_HAS_MESSAGE]) {
			//刷出队列中数据
            [self _flushMessageQueue];
        } else {
            NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@",
 CUSTOM_PROTOCOL_SCHEME, [url path]);
        }
        return NO;
    } else if (self.webViewDelegate) {
        return [self.webViewDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    } else {
        return YES;
    }
} 

使用示例

UI端

<span style="white-space: pre;">	</span>//给WebViewJavascriptBridgeReady事件注册一个Listener
	document.addEventListener('WebViewJavascriptBridgeReady', onBridgeReady, false)
    <span style="white-space: pre;">	</span>//事件的响应处理
	function onBridgeReady(event) {
		var bridge = event.bridge
		var uniqueId = 1
        <span style="white-space: pre;">	</span>//日志记录
		function log(message, data) {
			var log = document.getElementById('log')
			var el = document.createElement('div')
			el.className = 'logLine'
			el.innerHTML = uniqueId++ + '. ' + message + (data ? ': ' + JSON.stringify(data) : '')
			if (log.children.length) { log.insertBefore(el, log.children[0]) }
			else { log.appendChild(el) }
		}
        <span style="white-space: pre;">	</span>//初始化操作,并定义默认的消息处理逻辑
		bridge.init(function(message) {
			log('JS got a message', message)
		})
        <span style="white-space: pre;">	</span>//注册一个名为testJavascriptHandler的处理器,并定义用于响应的处理逻辑
		bridge.registerHandler('testJavascriptHandler', function(data, response) {
			log('JS handler testJavascriptHandler was called', data)
			response.respondWith({ 'Javascript Says':'Right back atcha!' })
		})

        <span style="white-space: pre;">	</span>//创建一个发送消息给native端的按钮
		var button = document.getElementById('buttons').appendChild(document.createElement('button'))
		button.innerHTML = 'Send message to ObjC'
		button.ontouchstart = function(e) {
			e.preventDefault()
            <span style="white-space: pre;">		</span>//发送消息
			bridge.send('Hello from JS button')
		}

		document.body.appendChild(document.createElement('br'))

        <span style="white-space: pre;">	</span>//创建一个用于调用native端处理器的按钮
		var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
		callbackButton.innerHTML = 'Fire testObjcCallback'
		callbackButton.ontouchstart = function(e) {
			e.preventDefault()
			log("Calling handler testObjcCallback")
            //调用名为testObjcCallback的native端处理器,并传递参数,同时设置回调处理逻辑
			bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
				log('Got response from testObjcCallback', response)
			})
		}
	} 

Native端

//实例化一个webview并加入到window中去
    UIWebView* webView = [[UIWebView alloc] initWithFrame:self.window.bounds];
    [self.window addSubview:webView];
    
    //启用日志记录
    [WebViewJavascriptBridge enableLogging];
    
    //实例化WebViewJavascriptBridge并定义native端的默认消息处理器
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView handler:^(id data, WVJBResponse *response) {
        NSLog(@"ObjC received message from JS: %@", data);
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ObjC got message from Javascript:" message:data 
delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alert show];
    }];
    
    //注册一个供UI端调用的名为testObjcCallback的处理器,并定义用于响应的处理逻辑
    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponse *response) {
        NSLog(@"testObjcCallback called: %@", data);
        [response respondWith:@"Response from testObjcCallback"];
    }];
    
    //发送一条消息给UI端并定义回调处理逻辑
    [_bridge send:@"A string sent from ObjC before Webview has loaded." responseCallback:^(id error, id responseData){
        if (error) { NSLog(@"Uh oh - I got an error: %@", error); }
        NSLog(@"objc got response! %@ %@", error, responseData);
    }];
    
    //调用一个在UI端定义的名为testJavascriptHandler的处理器,没有定义回调
    [_bridge callHandler:@"testJavascriptHandler" data:[NSDictionary dictionaryWithObject:@"before ready" forKey:@"foo"]];
    
    [self renderButtons:webView];
    [self loadExamplePage:webView];
    
    //单纯发送一条消息给UI端
    [_bridge send:@"A string sent from ObjC after Webview has loaded."]; 

项目运行截图:


 
分享到
 
 


android人机界面指南
Android手机开发(一)
Android手机开发(二)
Android手机开发(三)
Android手机开发(四)
iPhone消息推送机制实现探讨
手机软件测试用例设计实践
手机客户端UI测试分析
手机软件自动化测试研究报告
更多...   


Android高级移动应用程序
Android应用开发
Android系统开发
手机软件测试
嵌入式软件测试
Android软、硬、云整合


领先IT公司 android开发平台最佳实践
北京 Android开发技术进阶
某新能源领域企业 Android开发技术
某航天公司 Android、IOS应用软件开发
阿尔卡特 Linux内核驱动
艾默生 嵌入式软件架构设计
西门子 嵌入式架构设计
更多...