您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
浅析 Cordova for iOS
 
作者:zhenby's blog 发布于 2015-8-10
   次浏览      
 

Cordova,对这个名字大家可能比较陌生,大家肯定听过 PhoneGap 这个名字,Cordova 就是 PhoneGap 被 Adobe 收购后所改的名字。

Cordova 是一个可以让 JS 与原生代码(包括 Android 的 java,iOS 的 Objective-C 等)互相通信的一个库,并且提供了一系列的插件类,比如 JS 直接操作本地数据库的插件类。

这些插件类都是基于 JS 与 Objective-C 可以互相通信的基础的,这篇文章说说 Cordova 是如何做到 JS 与 Objective-C 互相通信的,解释如何互相通信需要弄清楚下面三个问题:

1.JS 怎么跟 Objective-C 通信

2.Objective-C 怎么跟 JS 通信

3.JS 请求 Objective-C,Objective-C 返回结果给 JS,这一来一往是怎么串起来的

Cordova 现在最新版本是 2.7.0,本文也是基于 2.7.0 版本进行分析的。

JS 怎么跟 Objective-C 通信

JS 与 Objetive-C 通信的关键代码如下:(点击代码框右上角的文件名链接,可直接跳转该文件在 github 的地址)

function iOSExec() {
...
if (!isInContextOfEvalJs && commandQueue.length == 1) {
// 如果支持 XMLHttpRequest,则使用 XMLHttpRequest 方式
if (bridgeMode != jsToNativeModes.IFRAME_NAV) {
// This prevents sending an XHR when there is already one being sent.
// This should happen only in rare circumstances (refer to unit tests).
if (execXhr && execXhr.readyState != 4) {
execXhr = null;
}
// Re-using the XHR improves exec() performance by about 10%.
execXhr = execXhr || new XMLHttpRequest();
// Changing this to a GET will make the XHR reach the URIProtocol on 4.2.
// For some reason it still doesn't work though...
// Add a timestamp to the query param to prevent caching.
execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true);
if (!vcHeaderValue) {
vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];
}
execXhr.setRequestHeader('vc', vcHeaderValue);
execXhr.setRequestHeader('rc', ++requestCount);
if (shouldBundleCommandJson()) {
// 设置请求的数据
execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());
}
// 发起请求
execXhr.send(null);
} else {
// 如果不支持 XMLHttpRequest,则使用透明 iframe 的方式,设置 iframe 的 src 属性
execIframe = execIframe || createExecIframe();
execIframe.src = "gap://ready";
}
}
...
}

JS 使用了两种方式来与 Objective-C 通信,一种是使用 XMLHttpRequest 发起请求的方式,另一种则是通过设置透明的 iframe 的 src 属性,下面详细介绍一下两种方式是怎么工作的:

XMLHttpRequest bridge

JS 端使用 XMLHttpRequest 发起了一个请求:

execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true); ,请求的地址是 /!gap_exec;并把请求的数据放在了请求的 header 里面,见这句代码:execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages()); 。

而在 Objective-C 端使用一个 NSURLProtocol 的子类来检查每个请求,如果地址是 /!gap_exec 的话,则认为是 Cordova 通信的请求,直接拦截,拦截后就可以通过分析请求的数据,分发到不同的插件类(CDVPlugin 类的子类)的方法中:

+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
NSURL* theUrl = [theRequest URL];
NSString* theScheme = [theUrl scheme];

// 判断请求是否为 /!gap_exec
if ([[theUrl path] isEqualToString:@"/!gap_exec"]) {
NSString* viewControllerAddressStr = [theRequest valueForHTTPHeaderField:@"vc"];
if (viewControllerAddressStr == nil) {
NSLog(@"!cordova request missing vc header");
return NO;
}
long long viewControllerAddress = [viewControllerAddressStr longLongValue];
// Ensure that the UCCDVViewController has not been dealloc'ed.
UCCDVViewController* viewController = nil;
@synchronized(gRegisteredControllers) {
if (![gRegisteredControllers containsObject:
[NSNumber numberWithLongLong:viewControllerAddress]]) {
return NO;
}
viewController = (UCCDVViewController*)(void*)viewControllerAddress;
}

// 获取请求的数据
NSString* queuedCommandsJSON = [theRequest valueForHTTPHeaderField:@"cmds"];
NSString* requestId = [theRequest valueForHTTPHeaderField:@"rc"];
if (requestId == nil) {
NSLog(@"!cordova request missing rc header");
return NO;
}
...
}
...
}

Cordova 中优先使用这种方式,Cordova.js 中的注释有提及为什么优先使用 XMLHttpRequest 的方式,及为什么保留第二种 iframe bridge 的通信方式:

// XHR mode does not work on iOS 4.2, so default to IFRAME_NAV for such devices.
// XHR mode’s main advantage is working around a bug in -webkit-scroll, which
// doesn’t exist in 4.X devices anyways

iframe bridge

在 JS 端创建一个透明的 iframe,设置这个 ifame 的 src 为自定义的协议,而 ifame 的 src 更改时,UIWebView 会先回调其 delegate 的 webView:shouldStartLoadWithRequest:navigationType: 方法,关键代码如下:

// UIWebView 加载 URL 前回调的方法,返回 YES,则开始加载此 URL,返回 NO,则忽略此 URL
- (BOOL)webView:(UIWebView*)theWebView
shouldStartLoadWithRequest:(NSURLRequest*)request
navigationType:(UIWebViewNavigationType)navigationType
{
NSURL* url = [request URL];

/*
* Execute any commands queued with cordova.exec() on the JS side.
* The part of the URL after gap:// is irrelevant.
*/
// 判断是否 Cordova 的请求,对于 JS 代码中 execIframe.src = "gap://ready" 这句
if ([[url scheme] isEqualToString:@"gap"]) {
// 获取请求的数据,并对数据进行分析、处理
[_commandQueue fetchCommandsFromJs];
return NO;
}
...
}

Objective-C 怎么跟 JS 通信

熟悉 UIWebView 用法的同学都知道 UIWebView 有一个这样的方法 stringByEvaluatingJavaScriptFromString:,这个方法可以让一个 UIWebView 对象执行一段 JS 代码,这样就可以达到 Objective-C 跟 JS 通信的效果,在 Cordova 的代码中多处用到了这个方法,其中最重要的两处如下:

获取 JS 的请求数据

- (void)fetchCommandsFromJs
{
// Grab all the queued commands from the JS side.
NSString* queuedCommandsJSON = [_viewController.webView
stringByEvaluatingJavaScriptFromString:
@"cordova.require('cordova/exec').nativeFetchMessages()"];

[self enqueCommandBatch:queuedCommandsJSON];
if ([queuedCommandsJSON length] > 0) {
CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by request.");
}
}

把 JS 请求的结果返回给 JS 端

- (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop
{
js = [NSString stringWithFormat:
@"cordova.require('cordova/exec').nativeEvalAndFetch(function(){ %@ })",
js];
if (scheduledOnRunLoop) {
[self evalJsHelper:js];
} else {
[self evalJsHelper2:js];
}
}

- (void)evalJsHelper2:(NSString*)js
{
CDV_EXEC_LOG(@"Exec: evalling: %@", [js substringToIndex:MIN([js length], 160)]);
NSString* commandsJSON = [_viewController.webView
stringByEvaluatingJavaScriptFromString:js];
if ([commandsJSON length] > 0) {
CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by chaining.");
}

[_commandQueue enqueCommandBatch:commandsJSON];
}

- (void)evalJsHelper:(NSString*)js
{
// Cycle the run-loop before executing the JS.
// This works around a bug where sometimes alerts() within callbacks can cause
// dead-lock.
// If the commandQueue is currently executing, then we know that it is safe to
// execute the callback immediately.
// Using (dispatch_get_main_queue()) does *not* fix deadlocks for some reaon,
// but performSelectorOnMainThread: does.
if (![NSThread isMainThread] || !_commandQueue.currentlyExecuting) {
[self performSelectorOnMainThread:@selector(evalJsHelper2:)
withObject:js
waitUntilDone:NO];
} else {
[self evalJsHelper2:js];
}
}

怎么串起来

先看一下 Cordova JS 端请求方法的格式:

// successCallback : 成功回调方法
// failCallback : 失败回调方法
// server : 所要请求的服务名字
// action : 所要请求的服务具体操作
// actionArgs : 请求操作所带的参数
cordova.exec(successCallback, failCallback, service, action, actionArgs);

传进来的这五个参数并不是直接传送给原生代码的,Cordova JS 端会做以下的处理:

1.会为每个请求生成一个叫 callbackId 的唯一标识:这个参数需传给 Objective-C 端,Objective-C 处理完后,会把 callbackId 连同处理结果一起返回给 JS 端

2.以 callbackId 为 key,{success:successCallback, fail:failCallback} 为 value,把这个键值对保存在 JS 端的字典里,successCallback 与 failCallback 这两个参数不需要传给 Objective-C 端,Objective-C 返回结果时带上 callbackId,JS 端就可以根据 callbackId 找到回调方法

3.每次 JS 请求,最后发到 Objective-C 的数据包括:callbackId, service, action, actionArgs
关键代码如下:

function iOSExec() {
...
// 生成一个 callbackId 的唯一标识,并把此标志与成功、失败回调方法一起保存在 JS 端
// Register the callbacks and add the callbackId to the positional
// arguments if given.
if (successCallback || failCallback) {
callbackId = service + cordova.callbackId++;
cordova.callbacks[callbackId] =
{success:successCallback, fail:failCallback};
}

actionArgs = massageArgsJsToNative(actionArgs);

// 把 callbackId,service,action,actionArgs 保持到 commandQueue 中
// 这四个参数就是最后发给原生代码的数据
var command = [callbackId, service, action, actionArgs];
commandQueue.push(JSON.stringify(command));
...
}

// 获取请求的数据,包括 callbackId, service, action, actionArgs
iOSExec.nativeFetchMessages = function() {
// Each entry in commandQueue is a JSON string already.
if (!commandQueue.length) {
return '';
}
var json = '[' + commandQueue.join(',') + ']';
commandQueue.length = 0;
return json;
};

原生代码拿到 callbackId、service、action 及 actionArgs 后,会做以下的处理:

1.根据 service 参数找到对应的插件类

2.根据 action 参数找到插件类中对应的处理方法,并把 actionArgs 作为处理方法请求参数的一部分传给处理方法

3.处理完成后,把处理结果及 callbackId 返回给 JS 端,JS 端收到后会根据 callbackId 找到回调方法,并把处理结果传给回调方法
关键代码:

- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId
{
CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status);
// This occurs when there is are no win/fail callbacks for the call.
if ([@"INVALID" isEqualToString : callbackId]) {
return;
}
int status = [result.status intValue];
BOOL keepCallback = [result.keepCallback boolValue];
NSString* argumentsAsJSON = [result argumentsAsJSON];

// 将请求的处理结果及 callbackId 通过调用 JS 方法返回给 JS 端
NSString* js = [NSString stringWithFormat:
@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d)",
callbackId, status, argumentsAsJSON, keepCallback];

[self evalJsHelper:js];
}


 // 根据 callbackId 及是否成功标识,找到回调方法,并把处理结果传给回调方法
callbackFromNative: function(callbackId, success, status, args, keepCallback) {
var callback = cordova.callbacks[callbackId];
if (callback) {
if (success && status == cordova.callbackStatus.OK) {
callback.success && callback.success.apply(null, args);
} else if (!success) {
callback.fail && callback.fail.apply(null, args);
}

// Clear callback if not expecting any more results
if (!keepCallback) {
delete cordova.callbacks[callbackId];
}
}
}

通信效率

Cordova 这套通信效率并不算低。我使用 iPod Touch 4 与 iPhone 5 进行真机测试:JS 做一次请求,Objective-C 收到请求后不做任何的处理,马上把请求的数据返回给 JS 端,这样能大概的测出一来一往的时间(从 JS 发出请求,到 JS 收到结果的时间)。每个真机我做了三组测试,每组连续测试十次,每组测试前我都会把机器重启,结果如下:

iPod Touch 4(时间单位:毫秒):

这三十次测试的平均时间是:(11.0 + 15.2 + 13.2) / 3 = 13.13 毫秒

iPhone 5(时间单位:毫秒)

这三十次测试的平均时间是:(2.7 + 2.8 + 2.7) / 3 = 2.73 毫秒

这通信的效率虽然比不上原生调原生,但是也是属于可接受的范围了。

   
次浏览       
 
相关文章

手机软件测试用例设计实践
手机客户端UI测试分析
iPhone消息推送机制实现与探讨
Android手机开发(一)
 
相关文档

Android_UI官方设计教程
手机开发平台介绍
android拍照及上传功能
Android讲义智能手机开发
相关课程

Android高级移动应用程序
Android系统开发
Android应用开发
手机软件测试
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

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

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

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