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

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

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

JS 怎么跟 Objective-C 通信

Objective-C 怎么跟 JS 通信

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

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

JS 怎么跟 Objective-C 通信

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

JS 发起请求 cordova.js

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 类的子类)的方法中:

UCCDVURLProtocol 拦截请求UCCDVURLProtocol.m

+ (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拦截加载CDVViewController.m

// 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 的请求数据

获取 JS 的请求数据CDVCommandQueue.m

- (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 端

把 JS 请求的结果返回给 JS 端CDVCommandDelegateImpl.m

- (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 端会做以下的处理:

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

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

每次 JS 请求,最后发到 Objective-C 的数据包括:callbackId, service, action, actionArgs

关键代码如下:

JS 端处理请求cordova.js

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 后,会做以下的处理:

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

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

处理完成后,把处理结果及 callbackId 返回给 JS 端,JS 端收到后会根据 callbackId 找到回调方法,并把处理结果传给回调方法

关键代码:

Objective-C 返回结果给 JS 端CDVCommandDelegateImpl.m

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

}

JS 端根据 callbackId 回调cordova.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(时间单位:毫秒):

组\序号 第1次 第2次 第3次 第4次 第5次 第6次 第7次 第8次 第9次 第10次 组平均时间

第一组 10 11 8 13 11 9 14 13 9 12 11.0

第二组 33 13 9 13 11 8 14 12 15 37 15.2

第三组 20 19 9 16 11 17 13 9 10 8 13.2

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

iPhone 5(时间单位:毫秒)

组\序号 第1次 第2次 第3次 第4次 第5次 第6次 第7次 第8次 第9次 第10次 组平均时间

第一组 3 3 4 2 3 2 3 2 2 3 2.7

第二组 7 2 2 2 2 3 2 2 2 4 2.8

第三组 6 3 2 3 2 2 2 3 2 2 2.7

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

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

最新文章

  1. nslookup命令
  2. 大话immutable.js
  3. Node创建TCP聊天
  4. 学习练习 java 不重复的三位偶数
  5. 第二百三十三天 how can I 坚持
  6. HDU 1999 不可摸数
  7. ExecutorService生命周期
  8. python中函数的默认参数陷阱问题
  9. HDU 5185 Equation (DP)
  10. Reliability diagrams
  11. java 易错选择题 编辑中
  12. 用Java开发一个工具类,提供似于js中eval函数功能的eval方法
  13. 基于“MVC”框架集设计模式,开发用户管理系统!
  14. no such file or directory, open '/node_modules/.staging/
  15. 常见的python的unittest用法
  16. 纯小白入手 vue3.0 CLI - 3.1 - 路由 ( router )
  17. socket编程-阻塞和非阻塞
  18. ssh免输入密码登录
  19. Centos6与Centos7防火墙设置与端口开放的方法
  20. 【转】PHP解析带CDATA的XML方法

热门文章

  1. Spring boot 2.0 学习
  2. petrozavodsk1
  3. wpf窗口禁止最大化但允许调整大小
  4. 《精通Spring4.X企业应用开发实战》读后感第三章
  5. 线性Softmax分类器实战
  6. 修改mac host文件绑定域名
  7. ue4 动画相关方法杂记
  8. codevs 3162 抄书问题
  9. 如何在VMware workstation上创建Linux虚拟机
  10. 剑指Offer的学习笔记(C#篇)-- 从上往下打印二叉树