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 的实现方式并不相同。
- var r = prompt(JSON.stringify(args), "gap:"+JSON.stringify([service, action, callbackId, true]));
这句 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 的注释中 , 作者写道
- The native side can return:
- Synchronous: PluginResult object as a JSON string
- 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 方法。如下代码是一个极为简单的实现本地启动界面的插件。
- package org.apache.cordova;
- import org.apache.cordova.api.Plugin;
- import org.apache.cordova.api.PluginResult;
- import org.json.JSONArray;
- public class SplashScreen extends Plugin {
- @Override
- public PluginResult execute(String action, JSONArray args, String callbackId) {
- PluginResult.Status status = PluginResult.Status.OK;
- String result = "";
- if (action.equals("hide")) {
- ((DroidGap)this.ctx).removeSplashScreen();
- }
- else {
- status = PluginResult.Status.INVALID_ACTION;
- }
- return new PluginResult(status, result);
- }
- }
上述实例展现了一个典型的 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 看起。
- public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
- PluginResult cr = null;
- boolean runAsync = async;
- try {
- final JSONArray args = new JSONArray(jsonArgs);
- final IPlugin plugin = this.getPlugin(service);
- final CordovaInterface ctx = this.ctx;
- if (plugin != null) {
- runAsync = async && !plugin.isSynch(action);
- if (runAsync) {
- // Run this on a different thread so that this one can return back to JS
- Thread thread = new Thread(new Runnable() {
- public void run() {
- try {
- // Call execute on the plugin so that it can do it's thing
- PluginResult cr = plugin.execute(action, args, callbackId);
- int status = cr.getStatus();
- // If no result to be sent and keeping callback, then no need to sent back to JavaScript
- if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
- }
- // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
- else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
- ctx.sendJavascript(cr.toSuccessCallbackString(callbackId));
- }
- // If error
- else {
- ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
- }
- } catch (Exception e) {
- PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
- ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
- }
- }
- });
- thread.start();
- return "";
- } else {
- // Call execute on the plugin so that it can do it's thing
- cr = plugin.execute(action, args, callbackId);
- // If no result to be sent and keeping callback, then no need to sent back to JavaScript
- if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
- return "";
- }
- }
- }
- } catch (JSONException e) {
- System.out.println("ERROR: " + e.toString());
- cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
- }
- // if async we have already returned at this point unless there was an error...
- if (runAsync) {
- if (cr == null) {
- cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
- }
- ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
- }
- 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 插件中看到这样一段代码 :
- /**
- * Create a new plugin result and send it back to JavaScript
- *
- * @param connection the network info to set as navigator.connection
- */
- private void sendUpdate(String type) {
- PluginResult result = new PluginResult(PluginResult.Status.OK, type);
- result.setKeepCallback(true);
- this.success(result, this.connectionCallbackId);
- // Send to all plugins
- 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 的通知后 , 怎么异步回调的呢 ?
/**
- * Updates the JavaScript side whenever the battery changes
- *
- * @param batteryIntent the current battery information
- * @return
- */
- private void updateBatteryInfo(Intent batteryIntent) {
- sendUpdate(this.getBatteryInfo(batteryIntent), true);
- }
- /**
- * Create a new plugin result and send it back to JavaScript
- *
- * @param connection the network info to set as navigator.connection
- */
- private void sendUpdate(JSONObject info, boolean keepCallback) {
- if (this.batteryCallbackId != null) {
- PluginResult result = new PluginResult(PluginResult.Status.OK, info);
- result.setKeepCallback(keepCallback);
- this.success(result, this.batteryCallbackId);
- }
- }
其最终调用了 success 方法。 Success 方法将 PluginResult 包装成回调语句 , 并通过 DroidGap 向CallBackServer sendJavaScript 。
由此为止,本地层的异步 sendJavaScript 已经完成了。接下来的问题便是 ,JS 层如何 getJavaScript 呢 ? 在lib/android/plugin/android/polling.js 中 , 可以看到 js 层获取回调的轮询实现。
- polling = function() {
- // Exit if shutting down app
- if (cordova.shuttingDown) {
- return;
- }
- // If polling flag was changed, stop using polling from now on and switch to XHR server / callback
- if (!cordova.UsePolling) {
- require('cordova/plugin/android/callback')();
- return;
- }
- var msg = prompt("", "gap_poll:");
- if (msg) {
- setTimeout(function() {
- try {
- var t = eval(""+msg);
- }
- catch (e) {
- console.log("JSCallbackPolling: Message from Server: " + msg);
- console.log("JSCallbackPolling Error: "+e);
- }
- }, 1);
- setTimeout(polling, 1);
- }
- else {
- setTimeout(polling, period);
- }
- };
通过 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 文件都是跨平台通用的。 ( 即使有差异应该也不大 )
简单介绍一下各个目录和一些关键组件 :
- cordova.js:拦截DOM,Window事件,加入自定义cordova事件,管理回调Javascript。
- scripts/require.js:PhoneGap中模块化机制的基础框架。简单但是也不简单!
- scripts/bootstrap.js:负责cordova的启动。
- common/channel.js:PhoneGap中实现事件监听的基础。
- common/builder.js:具备定制化构造模块的能力。
- common/plugin:如名字所示。这里放置所有平台通用的插件接口。
- lib/exec.js:于上一篇解析中提到。是Javascript调用Native的入口。
- lib/platform.js:与平台实现有关的初始化。
- lib/android/plugin:与android平台紧密相关的插件。
二、 浅析 PhoneGap 中的模块化机制
也许是因为本人见过的 Javascript 代码太少,见到 PhoneGap 的模块化机制后便觉得非常的有趣和前卫。Pascal 的作者沃斯曾写过一门叫做 module 的语言,其在语言级别做了模块化机制。我不知道 PhoneGap 模块化的思路是否也受 ; 此影响。
废话不多说了,从 require.js 开始看起吧。它是模块化的基础。
- var require,
- define;
- (function () {
- var modules = {};
- function build(module) {
- var factory = module.factory;
- module.exports = {};
- delete module.factory;
- factory(require, module.exports, module);
- return module.exports;
- }
- require = function (id) {
- if (!modules[id]) {
- throw "module " + id + " not found";
- }
- return modules[id].factory ? build(modules[id]) : modules[id].exports;
- };
- define = function (id, factory) {
- if (modules[id]) {
- throw "module " + id + " already defined";
- }
- modules[id] = {
- id: id,
- factory: factory
- };
- };
- define.remove = function (id) {
- delete modules[id];
- };
- })();
- //Export for use in node
- if (typeof module === "object" && typeof require === "function") {
- module.exports.require = require;
- module.exports.define = define;
- }
代码行数的确非常短。其定义了 require 和 define 两个函数。首先 define 函数用于声明一个模块。其中 id 表示模块名称,这必须是唯一的,而 factory 便是构造模块的工厂方法。 require 函数使用懒加载的方式获得已define 过的对应 id 的模块。
来看一个使用其的简单示例吧 :
- define("cordova/plugin/android/app", function(require, exports, module) {
- var exec = require('cordova/exec');
- module.exports = {
- /**
- * Clear the resource cache.
- */
- clearCache:function() {
- exec(null, null, "App", "clearCache", []);
- },
- /**
- * Load the url into the webview or into new browser instance.
- *
- * @param url The URL to load
- * @param props Properties that can be passed in to the activity:
- * wait: int => wait msec before loading URL
- * loadingDialog: "Title,Message" => display a native loading dialog
- * loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error
- * clearHistory: boolean => clear webview history (default=false)
- * openExternal: boolean => open in a new browser (default=false)
- *
- * Example:
- * navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000});
- */
- loadUrl:function(url, props) {
- exec(null, null, "App", "loadUrl", [url, props]);
- },
- /**
- * Cancel loadUrl that is waiting to be loaded.
- */
- cancelLoadUrl:function() {
- exec(null, null, "App", "cancelLoadUrl", []);
- },
- /**
- * Clear web history in this web view.
- * Instead of BACK button loading the previous web page, it will exit the app.
- */
- clearHistory:function() {
- exec(null, null, "App", "clearHistory", []);
- },
- /**
- * Go to previous page displayed.
- * This is the same as pressing the backbutton on Android device.
- */
- backHistory:function() {
- exec(null, null, "App", "backHistory", []);
- },
- /**
- * Override the default behavior of the Android back button.
- * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired.
- *
- * Note: The user should not have to call this method. Instead, when the user
- * registers for the "backbutton" event, this is automatically done.
- *
- * @param override T=override, F=cancel override
- */
- overrideBackbutton:function(override) {
- exec(null, null, "App", "overrideBackbutton", [override]);
- },
- /**
- * Exit and terminate the application.
- */
- exitApp:function() {
- return exec(null, null, "App", "exitApp", []);
- }
- };
- });
这是 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 中我们看到下面一段代码。
- channel.join(function() {
- channel.onDeviceReady.fire();
- }, 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 详解
首先贴上代码。
- (function (context) {
- var channel = require("cordova/channel"),
- _self = {
- boot: function () {
- /**
- * Create all cordova objects once page has fully loaded and native side is ready.
- */
- channel.join(function() {
- var builder = require('cordova/builder'),
- base = require('cordova/common'),
- platform = require('cordova/platform');
- // Drop the common globals into the window object, but be nice and don't overwrite anything.
- builder.build(base.objects).intoButDontClobber(window);
- // Drop the platform-specific globals into the window object
- // and clobber any existing object.
- builder.build(platform.objects).intoAndClobber(window);
- // Merge the platform-specific overrides/enhancements into
- // the window object.
- if (typeof platform.merges !== 'undefined') {
- builder.build(platform.merges).intoAndMerge(window);
- }
- // Call the platform-specific initialization
- platform.initialize();
- // Fire event to notify that all objects are created
- channel.onCordovaReady.fire();
- // Fire onDeviceReady event once all constructors have run and
- // cordova info has been received from native side.
- channel.join(function() {
- channel.onDeviceReady.fire();
- }, channel.deviceReadyChannelsArray);
- }, [ channel.onDOMContentLoaded, channel.onNativeReady ]);
- }
- };
- // boot up once native side is ready
- channel.onNativeReady.subscribeOnce(_self.boot);
- // _nativeReady is global variable that the native side can set
- // to signify that the native code is ready. It is a global since
- // it may be called before any cordova JS is ready.
- if (window._nativeReady) {
- channel.onNativeReady.fire();
- }
- }(window));
我们先来详细关注下, 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 字段表明。
- objects: {
- cordova: {
- path: 'cordova',
- children: {
- exec: {
- path: 'cordova/exec'
- }
- }
- },
根据上面的定义, 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 版本中移除。
- android phonegap
- PhoneGap Android
- Android phonegap 到iOS phonegap
- PhoneGap 06 PhoneGap Android平台配置
- PhoneGap 2.9、PhoneGap 3.4 android环境搭建
- PhoneGap android开发:PhoneGap基本介绍
- PhoneGap android方向解析
- Android PhoneGap简析
- PhoneGap android方向解析
- android下phonegap插件
- android phoneGap源码解析
- android之PhoneGap入门
- PhoneGap plugins for Android
- PhoneGap android web 中间件
- Cordova(PhoneGap)体系结构(Android)
- PhoneGap开发Android
- PhoneGap Android 环境配置
- Android PhoneGap简析
- 算法导论 第六章 堆排序
- Linux/ubuntu下core文件的产生与使用
- haha
- 计算个体积什么的
- [sicily online]1146. 采药(0-1背包)
- android phonegap
- 芬兰本土公司Jolla计划复兴诺基亚MeeGo系统
- Sencha谈论HTML5和CSS进程
- android中通过滚动条实现图片的渐变
- 关于Codeblocks+wxWidgets的使用问题
- 诺基亚CEO埃洛普的2012:煎熬中看到希望
- apache域名跳转
- mediarecorder调用流程
- “断网”日记:没有网络我们能干什么