1#

转自:http://www.cocoachina.com/applenews/devnews/2013/0520/6238.html

Cordova,对这个名字大家可能比较陌生,大家肯定听过 PhoneGap 这个名字,Cordova 就是 PhoneGap 被 Adobe 收购后所改的名字。(Cordova网址以及框架下载地址:http://cordova.apache.org/

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(github 地址)
  1. function iOSExec() {
  2.   ...
  3.   if (!isInContextOfEvalJs && commandQueue.length == 1)  {
  4.       // 如果支持 XMLHttpRequest,则使用 XMLHttpRequest 方式
  5.       if (bridgeMode != jsToNativeModes.IFRAME_NAV) {
  6.             // This prevents sending an XHR when there is already one being sent.
  7.             // This should happen only in rare circumstances (refer to unit tests).
  8.             if (execXhr && execXhr.readyState != 4) {
  9.                 execXhr = null;
  10.             }
  11.             // Re-using the XHR improves exec() performance by about 10%.
  12.             execXhr = execXhr || new XMLHttpRequest();
  13.             // Changing this to a GET will make the XHR reach the URIProtocol on 4.2.
  14.             // For some reason it still doesn't work though...
  15.             // Add a timestamp to the query param to prevent caching.
  16.             execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true);
  17.             if (!vcHeaderValue) {
  18.                 vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];
  19.             }
  20.             execXhr.setRequestHeader('vc', vcHeaderValue);
  21.             execXhr.setRequestHeader('rc', ++requestCount);
  22.             if (shouldBundleCommandJson()) {
  23.               // 设置请求的数据
  24.                 execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());
  25.             }
  26.             // 发起请求
  27.             execXhr.send(null);
  28.         } else {
  29.           // 如果不支持 XMLHttpRequest,则使用透明 iframe 的方式,设置 iframe 的 src 属性
  30.             execIframe = execIframe || createExecIframe();
  31.             execIframe.src = "gap://ready";
  32.         }
  33.     }
  34.   ...
  35. }
复制代码
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(github 地址)
  1. + (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
  2. {
  3.     NSURL* theUrl = [theRequest URL];
  4.     NSString* theScheme = [theUrl scheme];  // 判断请求是否为 /!gap_exec
  5.     if ([[theUrl path] isEqualToString:@"/!gap_exec"]) {
  6.         NSString* viewControllerAddressStr = [theRequest valueForHTTPHeaderField:@"vc"];
  7.         if (viewControllerAddressStr == nil) {
  8.             NSLog(@"!cordova request missing vc header");
  9.             return NO;
  10.         }
  11.         long long viewControllerAddress = [viewControllerAddressStr longLongValue];
  12.         // Ensure that the UCCDVViewController has not been dealloc'ed.
  13.         UCCDVViewController* viewController = nil;
  14.         @synchronized(gRegisteredControllers) {
  15.             if (![gRegisteredControllers containsObject:
  16.                   [NSNumber numberWithLongLong:viewControllerAddress]]) {
  17.                 return NO;
  18.             }
  19.             viewController = (UCCDVViewController*)(void*)viewControllerAddress;
  20.         }      // 获取请求的数据
  21.         NSString* queuedCommandsJSON = [theRequest valueForHTTPHeaderField:@"cmds"];
  22.         NSString* requestId = [theRequest valueForHTTPHeaderField:@"rc"];
  23.         if (requestId == nil) {
  24.             NSLog(@"!cordova request missing rc header");
  25.             return NO;
  26.         }
  27.           ...
  28.     }
  29.     ...
  30. }
复制代码
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(github 地址)
  1. // UIWebView 加载 URL 前回调的方法,返回 YES,则开始加载此 URL,返回 NO,则忽略此 URL
  2. - (BOOL)webView:(UIWebView*)theWebView
  3.           shouldStartLoadWithRequest:(NSURLRequest*)request
  4.           navigationType:(UIWebViewNavigationType)navigationType
  5. {
  6.     NSURL* url = [request URL];    /*
  7.      * Execute any commands queued with cordova.exec() on the JS side.
  8.      * The part of the URL after gap:// is irrelevant.
  9.      */
  10.     // 判断是否 Cordova 的请求,对于 JS 代码中 execIframe.src = "gap://ready" 这句
  11.     if ([[url scheme] isEqualToString:@"gap"]) {
  12.         // 获取请求的数据,并对数据进行分析、处理
  13.         [_commandQueue fetchCommandsFromJs];
  14.         return NO;
  15.     }
  16.     ...
  17. }
复制代码
二、Objective-C 怎么跟 JS 通信
熟悉 UIWebView 用法的同学都知道 UIWebView 有一个这样的方法 stringByEvaluatingJavaScriptFromString:,这个方法可以让一个 UIWebView 对象执行一段 JS 代码,这样就可以达到 Objective-C 跟 JS 通信的效果,在 Cordova 的代码中多处用到了这个方法,其中最重要的两处如下:

获取 JS 的请求数据

                                          获取 JS 的请求数据                                          CDVCommandQueue.m(github 地址)
  1. - (void)fetchCommandsFromJs
  2. {
  3.     // Grab all the queued commands from the JS side.
  4.     NSString* queuedCommandsJSON = [_viewController.webView
  5.                                       stringByEvaluatingJavaScriptFromString:
  6.                                           @"cordova.require('cordova/exec').nativeFetchMessages()"];    [self enqueCommandBatch:queuedCommandsJSON];
  7.     if ([queuedCommandsJSON length] > 0) {
  8.         CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by request.");
  9.     }
  10. }
复制代码
把 JS 请求的结果返回给 JS 端
                                          把 JS 请求的结果返回给 JS 端                          CDVCommandDelegateImpl.m(github 地址)
  1. - (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop
  2. {
  3.     js = [NSString stringWithFormat:
  4.                   @"cordova.require('cordova/exec').nativeEvalAndFetch(function(){ %@ })",
  5.                   js];
  6.     if (scheduledOnRunLoop) {
  7.         [self evalJsHelper:js];
  8.     } else {
  9.         [self evalJsHelper2:js];
  10.     }
  11. }- (void)evalJsHelper2:(NSString*)js
  12. {
  13.     CDV_EXEC_LOG(@"Exec: evalling: %@", [js substringToIndex:MIN([js length], 160)]);
  14.     NSString* commandsJSON = [_viewController.webView
  15.                               stringByEvaluatingJavaScriptFromString:js];
  16.     if ([commandsJSON length] > 0) {
  17.         CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by chaining.");
  18.     }    [_commandQueue enqueCommandBatch:commandsJSON];
  19. }- (void)evalJsHelper:(NSString*)js
  20. {
  21.     // Cycle the run-loop before executing the JS.
  22.     // This works around a bug where sometimes alerts() within callbacks can cause
  23.     // dead-lock.
  24.     // If the commandQueue is currently executing, then we know that it is safe to
  25.     // execute the callback immediately.
  26.     // Using    (dispatch_get_main_queue()) does *not* fix deadlocks for some reaon,
  27.     // but performSelectorOnMainThread: does.
  28.     if (![NSThread isMainThread] || !_commandQueue.currentlyExecuting) {
  29.         [self performSelectorOnMainThread:@selector(evalJsHelper2:)
  30.                               withObject:js
  31.                            waitUntilDone:NO];
  32.     } else {
  33.         [self evalJsHelper2:js];
  34.     }
  35. }
复制代码
三、怎么串起来
先看一下 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, failailCallback} 为 value,把这个键值对保存在 JS 端的字典里,successCallback 与 failCallback 这两个参数不需要传给 Objective-C 端,Objective-C 返回结果时带上 callbackId,JS 端就可以根据 callbackId 找到回调方法。
3.每次 JS 请求,最后发到 Objective-C 的数据包括:callbackId, service, action, actionArgs。

关键代码如下:
                                           JS 端处理请求                                                     cordova.js(github 地址)
  1. function iOSExec() {
  2.     ...
  3.   // 生成一个 callbackId 的唯一标识,并把此标志与成功、失败回调方法一起保存在 JS 端
  4.     // Register the callbacks and add the callbackId to the positional
  5.     // arguments if given.
  6.     if (successCallback || failCallback) {
  7.         callbackId = service + cordova.callbackId++;
  8.         cordova.callbacks[callbackId] =
  9.             {success:successCallback, fail:failCallback};
  10.     }    actionArgs = massageArgsJsToNative(actionArgs);  // 把 callbackId,service,action,actionArgs 保持到 commandQueue 中
  11.   // 这四个参数就是最后发给原生代码的数据
  12.     var command = [callbackId, service, action, actionArgs];
  13.     commandQueue.push(JSON.stringify(command));
  14.     ...
  15. }// 获取请求的数据,包括 callbackId, service, action, actionArgs
  16. iOSExec.nativeFetchMessages = function() {
  17.     // Each entry in commandQueue is a JSON string already.
  18.     if (!commandQueue.length) {
  19.         return '';
  20.     }
  21.     var json = '[' + commandQueue.join(',') + ']';
  22.     commandQueue.length = 0;
  23.     return json;
  24. };
复制代码
原生代码拿到 callbackId、service、action 及 actionArgs 后,会做以下的处理:

1.根据 service 参数找到对应的插件类
2.根据 action 参数找到插件类中对应的处理方法,并把 actionArgs 作为处理方法请求参数的一部分传给处理方法
3.处理完成后,把处理结果及 callbackId 返回给 JS 端,JS 端收到后会根据 callbackId 找到回调方法,并把处理结果传给回调方法

关键代码:
                                             Objective-C 返回结果给JS端                           CDVCommandDelegateImpl.m(github 地址)
- (void)sendPluginResultCDVPluginResult*)result callbackIdNSString*)callbackId
  1. {
  2.     CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status);
  3.     // This occurs when there is are no win/fail callbacks for the call.
  4.     if ([@"INVALID" isEqualToString : callbackId]) {
  5.         return;
  6.     }
  7.     int status = [result.status intValue];
  8.     BOOL keepCallback = [result.keepCallback boolValue];
  9.     NSString* argumentsAsJSON = [result argumentsAsJSON];  // 将请求的处理结果及 callbackId 通过调用 JS 方法返回给 JS 端
  10.     NSString* js = [NSString stringWithFormat:
  11.                               @"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d)",
  12.                               callbackId, status, argumentsAsJSON, keepCallback];    [self evalJsHelper:js];
  13. }
  14.                                              JS 端根据 callbackId 回调                              cordova.js(github 地址)// 根据 callbackId 及是否成功标识,找到回调方法,并把处理结果传给回调方法
  15. callbackFromNative: function(callbackId, success, status, args, keepCallback) {
  16.         var callback = cordova.callbacks[callbackId];
  17.         if (callback) {
  18.             if (success && status == cordova.callbackStatus.OK) {
  19.                 callback.success && callback.success.apply(null, args);
  20.             } else if (!success) {
  21.                 callback.fail && callback.fail.apply(null, args);
  22.             }            // Clear callback if not expecting any more results
  23.             if (!keepCallback) {
  24.                 delete cordova.callbacks[callbackId];
  25.             }
  26.         }
  27.     }
复制代码
通信效率
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 毫秒

这通信的效率虽然比不上原生调原生,但是也是属于可接受的范围了。
Cordova网址以及框架下载地址:http://cordova.apache.org/