传送门:
讨论q群:248908795
PhoneGap 源码解析
之前有一位前辈已经写了 PhoneGap android 源码的解析。但是,前辈写得比较简单,只是把通信原理提了一提。本篇源码解析,会对 PhoneGap 做一个全面的介绍。
关于 Java/JS 互调,鄙人接触也有一段时间了。在 android sdk 文档中 , 也有用 JsInterface loadUrl 做到交互的示例。但令我惊讶的是 ,PhoneGap 并没有选择用 JsInterface ,而是使用拦截 prompt 这种 hack 做法。
PhoneGap android 源码写得稍稍有点凌乱和啰嗦,后面会详细解析。好了,不废话了。开始正文了
一、JS 层与 Native 层之间通信原理
在讲解这部分之前,我先解释 PhoneGap 中的插件的概念。
Plugin: 插件。插件具备标准 js 没有的功能,如打电话、查看电池状态。这部分功能需要通过本地代码调用实现。每个插件都会对外提供至少一个方法。
lib/common/notification.js 这个插件。它具备了 alert,confirm,vibrate( 震动 ),beep( 蜂鸣 ) 这几个方法。
很显然,编写插件有两个要点。首先 , 需要编写一个实现插件功能的本地代码。其次,需要编写一个暴露调用接口的 js 代码来供使用插件者调用。
当编写完插件后,问题就来了。 Js 接口代码怎么去调用本地代码 ? 本地代码执行完毕后,怎么去回调 Js? 如何处理同步回调和异步回调 ? 这些通信问题的解决才是 PhoneGap 框架的精华所在。
下面我们逐一看看 phoneGap 是如何解决这些问题的。
      1. Js 接口代码怎么去调用本地代码 ?
lib/android/exec.js, 我们找到一个称为 exec 的关键模块。它是 js 层调用本地代码的入口。
它的定义是 exec(success, fail, service, action, args) 。顺便多说一句 , 虽然 exec PhoneGap 的一个关键模块,但由于受到平台差异影响,各个平台 exec 的实现方式并不相同。
 
 
Java代码  android PhoneGap源码详解_android PhoneGap源码详解
  1. var r = prompt(JSON.stringify(args), "gap:"+JSON.stringify([service, action, callbackId, true]));  
  2.    
 
 
这句 prompt 便实现了本地代码调用。本地代码通过 WebChromeClient 拦截 onJsPrompt 回调,利用 gap: 开头标志得知是调用本地插件请求 , 然后向 PluginManager 转发该请求。 PluginManager 将会根据参数来查找并执行具体插件方法。 关于 PluginManager, 后面会做更详细的解释。
      2. 本地代码怎么去回调 Js?
PhoneGap 并没有简单的用 loadUrl 来实现回调,而是在本地层建立了一个 CallBackServer 。由 Js 层不断向 CallBackServer 请求回调语句 , 然后 eval 执行该回调。
CallBackServer 提供了两种模式 , 一种是基于 XMLHttpRequst ,一种是基于轮询。 XHR 的方式即 js 层不断向 CallBackServer 发送 XMLHttpRequest 请求 , CallBackServer 则将回调语句返回给 js 层。
轮询方式则是 js 层通过 prompt 向本地发送 poll 请求 , 本地将从 CallBackServer 中拿出下一个回调返回给 js 层。
Js 层相关的 XHR 和轮询实现请参考 lib/android/plugin/android/callback.js, 以及 lib/android/plugin/android/polling.js
通过阅读 CallBackServer 的源码可知,当 url 为本地路径时,默认将启用 XHR 方式。
           3.  如何处理同步回调和异步回调 ?
先说同步处理。从 js prompt WebChromeClient onJSPrompt 是一个跨线程的同步调用。图示如下

android PhoneGap源码详解_android PhoneGap源码详解_02

通过 prompt 便可以直接得到 Plugin 执行的结果。后续做同步回调便也非常简单了。
接着再说说异步回调是如何实现的。注意在 exec.js 的注释中 , 作者写道
 
Java代码  android PhoneGap源码详解_android PhoneGap源码详解
  1. The native side can return:  
  2. Synchronous: PluginResult object as a JSON string  
  3. Asynchrounous: Empty string ""  
 
 
为了区别异步和同步。若 prompt 返回的是空字符串,那么将认为是异步调用。此时 PhoneGap 会在 JS 层保留回调函数,待本地层向 CallBackServer 发送回调后进行执行。
也许你会问 , 本地层怎么区别哪个回调啊 ?PhoneGap 对此的处理十分简单,在 cordova.js 中定义了一个 callbackId 的自增种子,并将每个 callBack 插入 callBacks 中去。无论同步异步,每个 plugin 调用都将得到一个流水号码作为回调标识。这个回调标识在 prompt 阶段便传递到了本地层。当本地层的 Plugin 异步结束后,便可以根据该 callbackId 找到回调。并向 CallBackServer 发送回调通知。图示如下

android PhoneGap源码详解_android PhoneGap源码详解_04
 
                           
二、 PhoneGap Native 层解析
与本地 Plugin 通信密切相关的是 :Plugin,PluginManager,PluginResult,CallbackServer
Plugin 是本地层所有插件的抽象基类,所有子插件都必须继承 Plugin 并实现 Plugin execute 方法。如下代码是一个极为简单的实现本地启动界面的插件。
 
Java代码  android PhoneGap源码详解_android PhoneGap源码详解
  1. package org.apache.cordova;  
  2.    
  3. import org.apache.cordova.api.Plugin;  
  4. import org.apache.cordova.api.PluginResult;  
  5. import org.json.JSONArray;  
  6.    
  7. public class SplashScreen extends Plugin {  
  8.    
  9.     @Override  
  10.     public PluginResult execute(String action, JSONArray args, String callbackId) {  
  11.         PluginResult.Status status = PluginResult.Status.OK;  
  12.         String result = "";  
  13.    
  14.         if (action.equals("hide")) {  
  15.             ((DroidGap)this.ctx).removeSplashScreen();  
  16.         }  
  17.         else {  
  18.             status = PluginResult.Status.INVALID_ACTION;  
  19.         }  
  20.         return new PluginResult(status, result);  
  21.     }  
  22. }  
 
 
上述实例展现了一个典型的 execute 处理流程。首先 , 根据 action 判断插件需要执行的动作方法,处理后返回一个 PluginResult
 
PluginResult 表示插件执行结果的实体。它主要包含了三个字段,分别是 status: 状态码, message,keepCallBack
最基本的 status 状态码分别是 OK( 成功 ),NO_RESULT( 没有结果 ),Error( 失败 ) ,另外 status 还定义许多失败的具体异常码。
message 是返回的结果实体, message 将作为参数传入回调函数中。
keepCallBack 表示是否需要保持回调。如果该项为 false ,那么在 JS 层在执行回调后将立即删除回调以释放资源。
其两个工具方法 :toSuccessCallBackString toErrorCallbackString 将生成一个 JS 回调语句。配合 CallBackServer 实现了 Native JS 回调。
 
所有的 Plugin 都由 PluginManager 托管。 Js 端调用 Native 代码时 ,onJSPrompt 会将请求转发给 PluginManager, PluginManager 便会负责查找并执行 Plugin
 
从上面所说可以看出, PluginManager 非常重要。首先 , 从最重要的 exec 看起。
 
Java代码  android PhoneGap源码详解_android PhoneGap源码详解
  1. public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {  
  2.     PluginResult cr = null;  
  3.     boolean runAsync = async;  
  4.     try {  
  5.         final JSONArray args = new JSONArray(jsonArgs);  
  6.         final IPlugin plugin = this.getPlugin(service);  
  7.         final CordovaInterface ctx = this.ctx;  
  8.         if (plugin != null) {  
  9.             runAsync = async && !plugin.isSynch(action);  
  10.             if (runAsync) {  
  11.                 // Run this on a different thread so that this one can return back to JS  
  12.                 Thread thread = new Thread(new Runnable() {  
  13.                     public void run() {  
  14.                         try {  
  15.                             // Call execute on the plugin so that it can do it's thing  
  16.                             PluginResult cr = plugin.execute(action, args, callbackId);  
  17.                             int status = cr.getStatus();  
  18.   
  19.                             // If no result to be sent and keeping callback, then no need to sent back to JavaScript  
  20.                             if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {  
  21.                             }  
  22.   
  23.                             // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)  
  24.                             else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {  
  25.                                 ctx.sendcr.toSuccessCallbackString(callbackId));  < /span>
  26.                             }  
  27.   
  28.                             // If error  
  29.                             else {  
  30.                                 ctx.sendcr.toErrorCallbackString(callbackId));  < /span>
  31.                             }  
  32.                         } catch (Exception e) {  
  33.                             PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());  
  34.                             ctx.sendcr.toErrorCallbackString(callbackId));  < /span>
  35.                         }  
  36.                     }  
  37.                 });  
  38.                 thread.start();  
  39.                 return "";  
  40.             } else {  
  41.                 // Call execute on the plugin so that it can do it's thing  
  42.                 cr = plugin.execute(action, args, callbackId);  
  43.   
  44.                 // If no result to be sent and keeping callback, then no need to sent back to JavaScript  
  45.                 if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {  
  46.                     return "";  
  47.                 }  
  48.             }  
  49.         }  
  50.     } catch (JSONException e) {  
  51.         System.out.println("ERROR: " + e.toString());  
  52.         cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);  
  53.     }  
  54.     // if async we have already returned at this point unless there was an error...  
  55.     if (runAsync) {  
  56.         if (cr == null) {  
  57.             cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);  
  58.         }  
  59.         ctx.sendcr.toErrorCallbackString(callbackId));  < /span>
  60.     }  
  61.     return (cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }");  
 
 
exec.js,PluginManager,Plugin 构成了经典的 Command/Action 模式。

android PhoneGap源码详解_android PhoneGap源码详解_07
exec.js 便对应着玉皇大帝,其面向的是 client, 期望调用的是具体 plugin( 美猴王 ) 的具体方法 ( 上天 ) 。然而 exec.js 只管向 PluginManager( 太白金星 ) 发送指示。 PluginManager( 太白金星 ) 管理所有的 Plugin( 小仙 ) 。它接到通知后,将会根据指示向具体的 Plugin 发出通知。具体的 Plugin( 美猴王 ) 接到通知后,执行动作 (execute) ,并根据 action 来区分具体操作。
由于 PluginManager 自身对所有的 Plugin 进行了管理,因此其可以很轻松的通过 service 找到对应的 Plugin 。然后想 Plugin 转发该 action
其中的 asyn 参数比较特殊,其封装了 Plugin 的异步执行模式。要想 Plugin execute 在线程中执行,必须具备两个条件。其一是 js “下旨”给 PluginManager 的时候表示希望异步调用。其二是 Plugin 自身是允许异步执行的。通过查看源代码,可以发现 js 端默认都是希望异步调用,因此是否开启异步模式将由 Plugin isSync 决定。
PluginManager 载入 Plugin 的方式其实非常简单。主要是通过读取 plugins.xml 中的配置。配置中的 name service 对应 ,value Plugin 的类路径对应。 PluginManager 载入 Plugin 是通过反射空构造器实现,因此需要特别注意自定义的 Plugin 不要有带参构造器。
PluginManager Plugin 的管理还包含广播生命周期以及广播消息的功能。其中生命周期方法 onResume,onPause,onDestroy 其实是和 web 页面生命周期密切相关的。 ( 而不是 Activity, 注意与 js 层的 onResume,onPause 有很大区别 !) 这点从 DroidGap loadUrlIntoView 中可以看出。至于广播消息,则是 Plugin 框架的一个比较有趣的地方。
我们在 NetWorkManager 插件中看到这样一段代码 :
 
Java代码  android PhoneGap源码详解_android PhoneGap源码详解
  1. /** 
  2.  * Create a new plugin result and send it back to JavaScript 
  3.  * 
  4.  * @param connection the network info to set as navigator.connection 
  5.  */  
  6. private void sendUpdate(String type) {  
  7.     PluginResult result = new PluginResult(PluginResult.Status.OK, type);  
  8.     result.setKeepCallback(true);  
  9.     this.success(result, this.connectionCallbackId);  
  10.      
  11.     // Send to all plugins  
  12.     this.ctx.postMessage("networkconnection", type);  
 
 
DroidGap 代理了 PluginManager postMessage 方法,此处实际是请求 PluginManager 向所有的 Plugin 广播网络切换的事件。如果其他的 Plugin 关心网络切换事件 , 只需要覆盖 onMessage 方法即可。这样就实现了 Plugin 插件之间的交互。
 
最后一块硬骨头是 CallBackServer 。代码行数其实一点也吓不倒人,短短 400 行而已。首先从轮询模式开讲,当载入的 url 不是本地页面时,由于受到跨域限制,将强制切换成轮询模式。注意 getJavascript sendJavascript 这两个方法。
前面说过, CallBackServer 是异步回调的基础。我们来看看轮询下的异步回调究竟是怎么玩儿的。
来看看 BatteryListener 插件 , 下面是它的 execute 方法
注意 action start 时候 PluginResult 的返回。它返回了 NO_Result keepCallback PluginResult exec.js 接到该返回后将保持该回调。在 start 的同时 ,batteryListener 还保存了 callbackId 。那么 , 当接到 BroadCastReceiver 的通知后 , 怎么异步回调的呢 ?
  /**
Java代码  android PhoneGap源码详解_android PhoneGap源码详解
  1.  * Updates the JavaScript side whenever the battery changes  
  2.  *  
  3.  * @param batteryIntent the current battery information  
  4.  * @return  
  5.  */  
  6. private void updateBatteryInfo(Intent batteryIntent) {     
  7.     sendUpdate(this.getBatteryInfo(batteryIntent), true);  
  8. }  
  9.   
  10. /** 
  11.  * Create a new plugin result and send it back to JavaScript 
  12.  * 
  13.  * @param connection the network info to set as navigator.connection 
  14.  */  
  15. private void sendUpdate(JSONObject info, boolean keepCallback) {  
  16.           if (this.batteryCallbackId != null) {  
  17.                    PluginResult result = new PluginResult(PluginResult.Status.OK, info);  
  18.                    result.setKeepCallback(keepCallback);  
  19.                    this.success(result, this.batteryCallbackId);  
  20.           }  
  21. }  
 
其最终调用了 success 方法。 Success 方法将 PluginResult 包装成回调语句 , 并通过 DroidGap CallBackServer sendJavaScript
由此为止,本地层的异步 sendJavaScript 已经完成了。接下来的问题便是 ,JS 层如何 getJavaScript ? lib/android/plugin/android/polling.js , 可以看到 js 层获取回调的轮询实现。
 
Java代码  android PhoneGap源码详解_android PhoneGap源码详解
  1.    
  2.    polling = function() {  
  3.       // Exit if shutting down app  
  4.       if (cordova.shuttingDown) {  
  5.           return;  
  6.       }  
  7.    
  8.       // If polling flag was changed, stop using polling from now on and switch to XHR server / callback  
  9.       if (!cordova.UsePolling) {  
  10.           require('cordova/plugin/android/callback')();  
  11.           return;  
  12.       }  
  13.    
  14.       var msg = prompt("""gap_poll:");  
  15.       if (msg) {  
  16.           setTimeout(function() {  
  17.               try {  
  18.                   var t = eval(""+msg);  
  19.               }  
  20.               catch (e) {  
  21.                   console.log("JSCallbackPolling: Message from Server: " + msg);  
  22.                   console.log("JSCallbackPolling Error: "+e);  
  23.               }  
  24.           }, 1);  
  25.           setTimeout(polling, 1);  
  26.       }  
  27.       else {  
  28.           setTimeout(polling, period);  
  29.       }  
  30. };  
 
 
通过 setTimeout 构成了一个死循环,通过 prompt 不断向本地层请求 gap_poll 。本地层收到 gap_poll 请求后,将会调用 CallBackServer getJavaScript 并同步返回给 polling Polling 接到回调后 , 通过 eval 便完成了 js 端回调代码的执行。
XHR 的方式与轮询其实类似 ,js 端的源码可以查看 lib/android/plugins/android/callback.js
最后给一张简单的静态结构图

android PhoneGap源码详解_android PhoneGap源码详解_11

 
CordovaInterface 中包含一些鸡肋的 url 白名单以及启动 Dialog 。虽然写得非常长 , 但如果了解整套 Plugin 机制的话,看下来还是小 case 的,这里就不赘述了。
三、 PhoneGap js 层源码
PhoneGap js 层源码的模块化机制和启动还是挺有趣的。下次码好字了传给大家看 J