android phonegap

来源:互联网 发布:php抓取url 编辑:程序博客网 时间:2024/04/27 17:49

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代码  收藏代码
  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 是一个跨线程的同步调用。图示如下




通过 prompt 便可以直接得到 Plugin 执行的结果。后续做同步回调便也非常简单了。

接着再说说异步回调是如何实现的。注意在 exec.js 的注释中 , 作者写道

 

Java代码  收藏代码
  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 发送回调通知。图示如下



 

                           

二、 PhoneGap Native 层解析

 与本地 Plugin 通信密切相关的是 :Plugin,PluginManager,PluginResult,CallbackServer 。

 Plugin 是本地层所有插件的抽象基类,所有子插件都必须继承 Plugin 并实现 Plugin 的 execute 方法。如下代码是一个极为简单的实现本地启动界面的插件。

 

Java代码  收藏代码
  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代码  收藏代码
  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.sendJavascript(cr.toSuccessCallbackString(callbackId));  
  26.                             }  
  27.   
  28.                             // If error  
  29.                             else {  
  30.                                 ctx.sendJavascript(cr.toErrorCallbackString(callbackId));  
  31.                             }  
  32.                         } catch (Exception e) {  
  33.                             PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());  
  34.                             ctx.sendJavascript(cr.toErrorCallbackString(callbackId));  
  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.sendJavascript(cr.toErrorCallbackString(callbackId));  
  60.     }  
  61.     return (cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }");  
 

 

 exec.js,PluginManager,Plugin 构成了经典的 Command/Action 模式。

以本篇日志中的图示为例 (http://www.cnblogs.com/springyangwc/archive/2011/04/13/2015456.html )



 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代码  收藏代码
  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代码  收藏代码
  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代码  收藏代码
  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 。

最后给一张简单的静态结构图




 

CordovaInterface 中包含一些鸡肋的 url 白名单以及启动 Dialog 。虽然写得非常长 , 但如果了解整套 Plugin 机制的话,看下来还是小 case 的,这里就不赘述了。

三、 PhoneGap 的 js 层源码

PhoneGap 的 js 层源码的模块化机制和启动还是挺有趣的。下次码好字了传给大家看 J

一、 Javascript 的源码结构

提醒一下大家 ,PhoneGap 的作者已经将 PhoneGap 的源码委托给了 Apache 基金会。 PhoneGap 的开源版本称为 cordova 。

PhoneGap 之于 Cordova ,正如 OpenJDK 之于 JDK 。两者基本上是差不多的。

cordova.android.js 是一个 build 版本。本人对其做了反 build 工作 ,cordova.android.js 展开的源码目录结构便如上所示。

从命名上其实已经可以看出 ,lib/android 属于 android 平台的专用库。其余平台基本上是 lib/windowsphone 或者 lib/ios 。而除 lib 下的 exec.js 和 platform.js 这两个是固定的。但是不同平台版本其实现是不一样的。除 lib包外,其余所有的 js 文件都是跨平台通用的。 ( 即使有差异应该也不大 )


简单介绍一下各个目录和一些关键组件 :

 

Java代码  收藏代码
  1. cordova.js:拦截DOM,Window事件,加入自定义cordova事件,管理回调Javascript。  
  2.   
  3. scripts/require.js:PhoneGap中模块化机制的基础框架。简单但是也不简单!  
  4.   
  5. scripts/bootstrap.js:负责cordova的启动。  
  6.   
  7. common/channel.js:PhoneGap中实现事件监听的基础。  
  8.   
  9. common/builder.js:具备定制化构造模块的能力。  
  10.   
  11. common/plugin:如名字所示。这里放置所有平台通用的插件接口。  
  12.   
  13. lib/exec.js:于上一篇解析中提到。是Javascript调用Native的入口。  
  14.   
  15. lib/platform.js:与平台实现有关的初始化。  
  16.   
  17. lib/android/plugin:与android平台紧密相关的插件。  

 

 

二、 浅析 PhoneGap 中的模块化机制

也许是因为本人见过的 Javascript 代码太少,见到 PhoneGap 的模块化机制后便觉得非常的有趣和前卫。Pascal 的作者沃斯曾写过一门叫做 module 的语言,其在语言级别做了模块化机制。我不知道 PhoneGap 模块化的思路是否也受 ; 此影响。

废话不多说了,从 require.js 开始看起吧。它是模块化的基础。

 

Js代码  收藏代码
  1. var require,  
  2.   
  3.     define;  
  4.   
  5.    
  6.   
  7. (function () {  
  8.   
  9.     var modules = {};  
  10.   
  11.    
  12.   
  13.     function build(module) {  
  14.   
  15.         var factory = module.factory;  
  16.   
  17.         module.exports = {};  
  18.   
  19.         delete module.factory;  
  20.   
  21.         factory(require, module.exports, module);  
  22.   
  23.         return module.exports;  
  24.   
  25.     }  
  26.   
  27.    
  28.   
  29.     require = function (id) {  
  30.   
  31.         if (!modules[id]) {  
  32.   
  33.             throw "module " + id + " not found";  
  34.   
  35.         }  
  36.   
  37.         return modules[id].factory ? build(modules[id]) : modules[id].exports;  
  38.   
  39.     };  
  40.   
  41.    
  42.   
  43.     define = function (id, factory) {  
  44.   
  45.         if (modules[id]) {  
  46.   
  47.             throw "module " + id + " already defined";  
  48.   
  49.         }  
  50.   
  51.    
  52.   
  53.         modules[id] = {  
  54.   
  55.             id: id,  
  56.   
  57.             factory: factory  
  58.   
  59.         };  
  60.   
  61.     };  
  62.   
  63.    
  64.   
  65.     define.remove = function (id) {  
  66.   
  67.         delete modules[id];  
  68.   
  69.     };  
  70.   
  71.    
  72.   
  73. })();  
  74.   
  75.    
  76.   
  77. //Export for use in node  
  78.   
  79. if (typeof module === "object" && typeof require === "function") {  
  80.   
  81.     module.exports.require = require;  
  82.   
  83.     module.exports.define = define;  
  84.   
  85. }  

 

 

代码行数的确非常短。其定义了 require 和 define 两个函数。首先 define 函数用于声明一个模块。其中 id 表示模块名称,这必须是唯一的,而 factory 便是构造模块的工厂方法。 require 函数使用懒加载的方式获得已define 过的对应 id 的模块。

来看一个使用其的简单示例吧 :

 

Java代码  收藏代码
  1. define("cordova/plugin/android/app", function(require, exports, module) {  
  2.   
  3. var exec = require('cordova/exec');  
  4.   
  5.    
  6.   
  7. module.exports = {  
  8.   
  9.   /** 
  10.  
  11.    * Clear the resource cache. 
  12.  
  13.    */  
  14.   
  15.   clearCache:function() {  
  16.   
  17.     exec(nullnull"App""clearCache", []);  
  18.   
  19.   },  
  20.   
  21.    
  22.   
  23.   /** 
  24.  
  25.    * Load the url into the webview or into new browser instance. 
  26.  
  27.    * 
  28.  
  29.    * @param url           The URL to load 
  30.  
  31.    * @param props         Properties that can be passed in to the activity: 
  32.  
  33.    *      wait: int                           => wait msec before loading URL 
  34.  
  35.    *      loadingDialog: "Title,Message"      => display a native loading dialog 
  36.  
  37.    *      loadUrlTimeoutValue: int            => time in msec to wait before triggering a timeout error 
  38.  
  39.    *      clearHistory: boolean              => clear webview history (default=false) 
  40.  
  41.    *      openExternal: boolean              => open in a new browser (default=false) 
  42.  
  43.    * 
  44.  
  45.    * Example: 
  46.  
  47.    *      navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000}); 
  48.  
  49.    */  
  50.   
  51.   loadUrl:function(url, props) {  
  52.   
  53.     exec(nullnull"App""loadUrl", [url, props]);  
  54.   
  55.   },  
  56.   
  57.    
  58.   
  59.   /** 
  60.  
  61.    * Cancel loadUrl that is waiting to be loaded. 
  62.  
  63.    */  
  64.   
  65.   cancelLoadUrl:function() {  
  66.   
  67.     exec(nullnull"App""cancelLoadUrl", []);  
  68.   
  69.   },  
  70.   
  71.    
  72.   
  73.   /** 
  74.  
  75.    * Clear web history in this web view. 
  76.  
  77.    * Instead of BACK button loading the previous web page, it will exit the app. 
  78.  
  79.    */  
  80.   
  81.   clearHistory:function() {  
  82.   
  83.     exec(nullnull"App""clearHistory", []);  
  84.   
  85.   },  
  86.   
  87.    
  88.   
  89.   /** 
  90.  
  91.    * Go to previous page displayed. 
  92.  
  93.    * This is the same as pressing the backbutton on Android device. 
  94.  
  95.    */  
  96.   
  97.   backHistory:function() {  
  98.   
  99.     exec(nullnull"App""backHistory", []);  
  100.   
  101.   },  
  102.   
  103.    
  104.   
  105.   /** 
  106.  
  107.    * Override the default behavior of the Android back button. 
  108.  
  109.    * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired. 
  110.  
  111.    * 
  112.  
  113.    * Note: The user should not have to call this method.  Instead, when the user 
  114.  
  115.    *       registers for the "backbutton" event, this is automatically done. 
  116.  
  117.    * 
  118.  
  119.    * @param override        T=override, F=cancel override 
  120.  
  121.    */  
  122.   
  123.   overrideBackbutton:function(override) {  
  124.   
  125.     exec(nullnull"App""overrideBackbutton", [override]);  
  126.   
  127.   },  
  128.   
  129.    
  130.   
  131.   /** 
  132.  
  133.    * Exit and terminate the application. 
  134.  
  135.    */  
  136.   
  137.   exitApp:function() {  
  138.   
  139.     return exec(nullnull"App""exitApp", []);  
  140.   
  141.   }  
  142.   
  143. };  
  144.   
  145. });  

 

 

这是 phonegap 模块化编程的典型写法。首先在头部定义依赖的模块组件 , 再次通过设置 module.exports 向外部暴露出对应的方法。

         由于 Javascript 中在语法级别没有私有访问符。因此往往解决之道是: a. 模仿 C 风格,命名使用 _ 开头的一律表示是私有 ;b. 新建一个 private 对象 , 将其私有方法放置其中 , 起到命名空间的作用 ;c. 将私有部分用 function{} 套住 , 用 return 返回公开部分。这种做法充分发挥了 javascript 闭包的优势,但是可读性比较差。

         phoneGap 的 module 机制使用了第三种做法。但是通过将构造和依赖分离开来,使得可读性大大增加。代码清晰好懂,依赖性也一目了然。

         也许有朋友要问。这种模块机制碰到循环依赖的情况怎么办?例如 A 在 define 阶段 requireB , B 在 define阶段又 requireA 。很遗憾,这种循环依赖的情况 依照 phoneGap 的模块化思路是无法实现的。因此对于关键组件的编制 ,phoneGap 总会小心翼翼地处理其依赖顺序。

         通过阅读源码,发现大致的模块依赖顺序是这样子的 ( 被依赖模块到依赖模块 ):utils.js->channel.js|builder.js->cordova.js->exec.js|polling.js->callback.js->platform.js->bootstrap.js 。

三、 PhoneGap 中的事件处理机制

common/channel.js 是 PhoneGap 事件处理机制的基础。每个事件类型,都会包装成一个 Channel 对象。

既然是事件,那么就得支持基础的观察者模式吧? Channel 的 prototype 定义了事件的一些关键方法。subscribe 用于注册一个监听器,并给监听器一个 guid 。 guid 类似 cordova.js 中的 callbackId ,只是一个流水标识。但 Channel 中的 guid 稍有些不同,指定确切的 guid 可以对监听器做覆盖操作。

utils.close 是个很有趣的方法。 Javascript 中的调用不当引起 this 不对,这是新手常见的错误。常见的做法会通过封装 apply 做 delegate 。而 close 这个方法是绝了 , 它通过闭包包装了一个指向确定 this ,调用确定function, 使用确定实参的 final 函数。不管在什么样的环境下调用 , 这个方法总能正确执行。

subscribeOnce 类似于 YUI 或者 jquery 中的 one 。只会收到一次监听。 ( 若事件已经触发过,则在注册阶段立即回调监听 )

unsubscribe 和 fire 分别用来注销监听器和触发事件。触发事件将会引起监听器的广播操作。可选的 fireArgs用于保证 subscribeOnce 在事件已触发的情况下能获得正确的广播参数。

Channel 本身还有一个监听注册 / 注销的事件拦截。分别是 onSubscribe 和 onUnSubscribe 。在common\plugin\battery.js 中,我们可以看到。 battery.js 便是利用这个注册监听回调,来对 Plugin 服务做懒加载和卸载工作。

作为模块暴露公有部分的 channel 对象比较有意思。 join 这个工具方法类似 subscribeOnce, 它的第二个参数是个 Channel 数组。当且仅当所有的 Channel 事件都被 fire 后 ,join 的监听才会被回调。这个方法还是挺有用的

create 是个构造工厂方法。新构造的 Channel 事件会被放置在 channel 对象中。使用上会方便点。在channel.create('onCordovaReady'); 后 , 便可以便捷的通过 channel[‘onCordovaReady’] 来方便的访问对应类型的Channel 对象了。

deviceReadyMap,deviceReadyArray,waitForInitialization,initializeComplete 这四者紧密相关。它们决定了onDeviceReady 事件在何时被触发。于 common/bootstrap.js 中我们看到下面一段代码。

 

Java代码  收藏代码
  1. channel.join(function() {  
  2.   
  3. channel.onDeviceReady.fire();  
  4.   
  5. }, channel.deviceReadyChannelsArray);  

 

         waitForInitialization 用于添加 onDeviceReady 的等待 Channel 事件。 initializeComplete 用于触发指定的等待 Channel 事件。如果想要增加 onDeviceReady 的条件,我们只需要在 onCordovaReady 之前添加waitForInitialization 即可。事实上,在 lib/android/plugin/storage.js 中我们便可以看到一个绝佳的例子。cupcakeStorage 利用本地 Plugin 为不支持 localStorage API 的 WebView 提供了一个备选方案。在本地建立好备用的 sqlite 数据库后 ,cupcakeStorage 的等待时间便结束完毕。

四、 启动与 PhoneGap 自定义事件

首先上图。


上图为本人整理的启动事件序列,待会儿大家便能从源码中看到了。

待续。。。。

一、 PhoneGap 的启动

近期因为赶项目和犯懒,所以一直没有更新,希望朋友们见谅。

前面介绍过 phoneGap 的模块化机制。其通过 require 和 define ,巧妙的将模块的定义和依赖关系隔离开来。


如上图所示,在定义模块机制后。后面统统都是做 define 。那么,这些被定义的模块最终何时被实例化呢?


在 cordova.js 的末尾我们找到了这一句。第一个被请求实例化的模块是 cordova(adobe 版本是 phonegap) ,接着在实例化 cordova 模块后, cordova 依赖的模块也纷纷被实例化出来了。 (channel 、 utils)

cordova 模块仅是个最基础的顶层通信模块。除了 cordova 模块外,其余数十个 phonegap 模块都还在沉睡之中。事实上,这一步走完其实 phonegap 已经完成了。 Phonegap 用户只需要 require(‘cordova/plugin/contacts’)这样,即可访问到 phonegap 的功能。

但是,这样做还不够好。第一 ,require 对于 phonegap 使用者来说并不友好。第二,虽然可以通过 require 得到构建模块的实例,可这并不代表 Native/Javascript 的通信关系已经建立。 phonegap 用户还不知道何时可以安全正确的使用这些功能。

基于上面两点, phonegap 构建了一个 bootstrap 函数。在上图的后一句,大家可以看到 bootstrap 的封闭调用。下面我来详解一下 bootstrap

二、 Bootstrap 详解

首先贴上代码。

 

Java代码  收藏代码
  1. (function (context) {  
  2.   
  3.     var channel = require("cordova/channel"),  
  4.   
  5.         _self = {  
  6.   
  7.             boot: function () {  
  8.   
  9.                 /** 
  10.  
  11.                  * Create all cordova objects once page has fully loaded and native side is ready. 
  12.  
  13.                  */  
  14.   
  15.                 channel.join(function() {  
  16.   
  17.                     var builder = require('cordova/builder'),  
  18.   
  19.                         base = require('cordova/common'),  
  20.   
  21.                         platform = require('cordova/platform');  
  22.   
  23.    
  24.   
  25.                     // Drop the common globals into the window object, but be nice and don't overwrite anything.  
  26.   
  27.                     builder.build(base.objects).intoButDontClobber(window);  
  28.   
  29.    
  30.   
  31.                     // Drop the platform-specific globals into the window object  
  32.   
  33.                     // and clobber any existing object.  
  34.   
  35.                     builder.build(platform.objects).intoAndClobber(window);  
  36.   
  37.    
  38.   
  39.                     // Merge the platform-specific overrides/enhancements into  
  40.   
  41.                     // the window object.  
  42.   
  43.                     if (typeof platform.merges !== 'undefined') {  
  44.   
  45.                         builder.build(platform.merges).intoAndMerge(window);  
  46.   
  47.                     }  
  48.   
  49.    
  50.   
  51.                     // Call the platform-specific initialization  
  52.   
  53.                     platform.initialize();  
  54.   
  55.    
  56.   
  57.                     // Fire event to notify that all objects are created  
  58.   
  59.                     channel.onCordovaReady.fire();  
  60.   
  61.    
  62.   
  63.                     // Fire onDeviceReady event once all constructors have run and  
  64.   
  65.                     // cordova info has been received from native side.  
  66.   
  67.                     channel.join(function() {  
  68.   
  69.                         channel.onDeviceReady.fire();  
  70.   
  71.                     }, channel.deviceReadyChannelsArray);  
  72.   
  73.    
  74.   
  75.                 }, [ channel.onDOMContentLoaded, channel.onNativeReady ]);  
  76.   
  77.             }  
  78.   
  79.         };  
  80.   
  81.    
  82.   
  83.     // boot up once native side is ready  
  84.   
  85.     channel.onNativeReady.subscribeOnce(_self.boot);  
  86.   
  87.    
  88.   
  89.     // _nativeReady is global variable that the native side can set  
  90.   
  91.     // to signify that the native code is ready. It is a global since  
  92.   
  93.     // it may be called before any cordova JS is ready.  
  94.   
  95.     if (window._nativeReady) {  
  96.   
  97.         channel.onNativeReady.fire();  
  98.   
  99.     }  
  100.   
  101.    
  102.   
  103. }(window));  
  104.   
  105.    
 

我们先来详细关注下, phonegap 是如何提供回调通知,来告诉用户 Native 与 Javascript 之间成功建立联系的呢?

首先, bootstrap 通过 channel 模块注册监听了 onNativeReady 事件。这个事件由 Native 层触发,用来表示Native 层准备完毕 ( 可以接受 plugin 调用 ) 。在 Android 平台上面 ,onNativeReady 是在 WebView 的onPageFinished 回调中触发的。


  为了安全的启动 ,boot 等待 onNativeReady 和 onDOMContentLoaded 完毕后才执行。那么 Phonegap 的boot 都做了些什么呢 ?

boot 做了两件事情。首先是实例化和发布模块来给 phonegap 用户使用。其次是广播 onCordovaReady 来通知 phonegap 层 boot 完毕 , 用户可以放心使用 phonegap 的功能。

通过 builder, 模块的实例化和发布工作变得很艺术。 builder 模块提供三种发布方式 , 分别是intoButDontClobber( 若发布目标中已存在同名模块 , 则不允许做覆盖 ),intoAndClobber( 若发布目标中已存在同名模块 , 则强制覆盖 ),intoAndMerge( 若发布目标中已存在实例模块 , 则尝试对两者进行合并,合并的优先级是欲发布模块比发布模块高 )

解释一下“发布”这个词。其实就是根据定义 id 实例化 (require) 模块 , 然后把它作为某个 object 的属性。从boot 的源码中,我们可以看到。在 common.js 和 platform.js 中,分别定义了 objects 对象。 objects 定义了各个模块的 id 和层次关系。父子关系通过 children 字段表明。

 

Java代码  收藏代码
  1. objects: {  
  2.   
  3.     cordova: {  
  4.   
  5.         path: 'cordova',  
  6.   
  7.         children: {  
  8.   
  9.             exec: {  
  10.   
  11.                 path: 'cordova/exec'  
  12.   
  13.             }  
  14.   
  15.         }  
  16.   
  17.     },  

 

         根据上面的定义, cordova 模块下将挂一个 exec 模块。即通过 builder 后 , 用户可以直接通过cordova.exec 来访问 exec 模块。

         当然 , 发布的 target 都是 window 。 common 下的发布定义 (objects) ,所使用的发布策略是不覆写原有属性。之所以这样做,是考虑到之后浏览器加强 html5 支持后,有些模块将会原生提供。

platform 下的发布定义 (objects) 所使用的发布策略是覆写所有属性。其定义的是与平台密切相关的模块,因此会做强制覆写。

注意 platform 下的发布定义,除了 objects 外。还提供了 merge 定义。这里的 merge 作用在于“增强”。为原有的通用模块,增加一些平台相关的其它方法。在 android/platform.js 的 merges 定义便是一个例子。最终发布的notification 将既包含 common 下 notification.js 的 alert 方法,也包含 platform 下 notification.js 的 activityStart 方法。

发布模块后会执行 platform.initialize 方法。这个方法用于做 platform 初始化工作,将与平台特性紧耦合,尤其会与 Native 与 Javascript 互相通信相关。在 android 平台版本中 ,initialize 包含了 polling/xhr 的初始化, android的按键事件、可兼容 web 数据库 api 与 localstorage 。当这些工作完毕后,将会广播 onCordovaReady 事件。此后便可以安全的使用 phonegap 所提供的所有功能了。

onCordovaReady 后会去准备 onDeviceReady 。关于 onDeviceReady 的详细描述,我在上篇解析的channel 中提过了,在此也就不重复了。

三、 总结

至此 phonegap 的 Native 与 Javascript 层源码解析完毕。有关如何扩展 phonegap 插件,各位可以参照github 上面的这个开源项目: https://github.com/purplecabbage/phonegap-plugins

需要注意的是。其中的 addPlugin 写法 ,phonegap 已经不再推荐使用了。并表示该方法将在 2.0 版本中移除。


原创粉丝点击