传送门:
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
相关推荐
在本篇《Android PhoneGap源码详解(二)》中,我们将深入探讨PhoneGap这一混合移动应用开发框架的内部机制,以及如何利用它来构建原生功能丰富的Android应用。PhoneGap是一个基于Apache Cordova的开源框架,允许...
【标题】:“Android + PhoneGap + jQuery Mobile” 这个项目标题揭示了一个使用三种技术栈构建的移动应用程序:Android、PhoneGap和jQuery Mobile。Android是Google主导的开源操作系统,主要用于智能手机和平板...
**Android PhoneGap 入门详解** PhoneGap 是一个开源框架,它允许开发者使用 HTML、CSS 和 JavaScript 这些Web技术来构建原生的移动应用程序。它基于 Apache Cordova 平台,通过桥接机制,将JavaScript 与移动设备...
PhoneGap Splashscreen 知识点详解 PhoneGap 是一个开源框架,它允许开发者使用HTML、CSS和JavaScript构建原生移动应用程序。PhoneGap Splashscreen 模块则是 PhoneGap 的一个重要组件,用于在应用启动时显示一个...
### 初试PhoneGap开发框架知识点详解 #### 一、概览 本篇文章将通过一个实战案例介绍如何使用PhoneGap开发框架来构建跨平台移动应用。PhoneGap是一款开放源码的移动开发工具,允许开发者利用HTML、CSS以及...
- **plugins**:这个文件夹可能包含了已安装的PhoneGap插件,每个插件有自己的目录,包含必要的源码和配置文件,用于扩展PhoneGap的功能。 3. **开发流程**: - **安装和配置环境**:开发者需要安装PhoneGap/...
PhoneGap 插件开发详解 PhoneGap 是一个开源框架,它允许开发者使用 HTML、CSS 和 JavaScript 来构建原生移动应用程序。PhoneGap 的核心理念是通过 Web 技术来实现跨平台的移动应用开发,同时利用各平台的原生功能...
总之,Android Photogap是利用PhoneGap框架开发的Android应用实例,通过理解和学习其源码,开发者可以更好地掌握混合应用开发技术,结合Web的便捷性和Android的广泛设备支持,高效地创建跨平台的应用程序。
- 操作系统和开放联盟:讨论PhoneGap与Android操作系统的关系,以及其在开放源码社区的角色。 - 接口和工具:学习使用PhoneGap所需的开发环境和工具,如Android SDK等。 2. **HTML5 API和Event事件** - HTML5...
**CordovaTest项目详解** **一、Cordova简介** Cordova,原名为PhoneGap,是一款开源的移动应用开发框架,允许开发者使用HTML5、CSS3和JavaScript来构建跨平台的移动应用程序。它将Web应用程序封装在原生的移动...
《构建Android日历插件:Calendar-for-Android详解》 在移动应用开发中,与用户交互的组件至关重要,其中日历功能就是一个常见的需求。本文将深入探讨名为"Calendar-for-Android"的Cordova插件,它允许开发者在...
**标题解析:** "CalculadoraJSAndroid"是一个项目名称,暗示了我们正在讨论一个使用JavaScript构建的Android应用程序,它...通过深入研究项目源码,开发者可以学习到如何在Android平台上利用JavaScript实现复杂功能。
### 二开苹果CMS视频网站源码模板及封装双端APP技术详解 #### 一、苹果CMS概述 苹果CMS(Apple Content Management System)是一款广泛应用于搭建视频网站的开源内容管理系统。它以其简单易用、功能强大著称,在...
**SignalStrength 插件详解** 在移动应用开发中,了解设备的信号强度是十分重要的,尤其是在涉及通信质量或网络性能优化的情景下。SignalStrength 插件是针对 PhoneGap 平台的一个实用工具,它允许开发者获取设备的...
《CorDova技术详解:构建跨平台H5与Native交互桥梁》 在移动应用开发领域,跨平台成为了不可或缺的需求。CorDova,作为一款强大的开源框架,为开发者提供了实现HTML5(H5)、JavaScript和CSS3等Web技术构建原生移动...
PhoneGap将这些Web应用封装在原生的移动应用壳中,使得它们可以在各大移动操作系统(如iOS、Android和Windows Phone)上运行,提供了一种跨平台的开发方式。QuizApp使用PhoneGap,意味着它的代码base是基于Web的,但...
**ToastyPlugin:Cordova插件详解** Cordova是一款开源的移动应用开发框架,它允许开发者使用HTML、CSS和JavaScript来构建原生的移动应用程序。Cordova通过将这些Web技术与设备APIs相结合,使开发者能够访问手机...