移动应用开发已经成为软件开发的一个重要方向,但是移动开发面临的一个重要问题就是跨平台的问题。PhoneGap 作为一个多平台的软件开发框架,提供了一次编写多个平台的运行。目前已经支持多达 6 个移动平台。对应平台的插件开发则可以为 PhoneGap 扩展 API 的本地接口,提供更加复杂的特性。了解 PhoneGap 框架的原理以便为 PhoneGap 提供本地的插件接口成为 PhoneGap 应用开发的重要方面。本文将以一个简单的实例介绍 PhoneGap 在 Android 上的插件开发方法。阅读本文需要基本的 Android 开发经验和 PhoneGap 开发经验。
PhoneGap 简介
PhoneGap 是利用 HTML 来开发移动应用的一个开放源代码的框架,利用它就可以仅仅使用 HTML 和 Javascript 实现一次编写,多个移动平台运行的应用。目前已经可以支持 Android、iOS、BlackBerry 等 6 个平台。在完成这篇短文的过程中,PhoneGap 正式发布了 1.0.0 版本。因而短文中的版本使用的是 1.0.0rc2 版本的代码。
PhoneGap 在 Android 平台上的实现的框架是利用了在本地代码和浏览器间建立中间层来实现对 Java 代码的隔离。利用浏览器的接口和网络 socket 接口实现数据的通讯。因此在对 PhoneGap 框架的理解上,需要从 Java 端和浏览器端两部分来分别学习。
关于 Android 开发环境和 PhoneGap 开发环境的搭建,请参考参考资料中的开发者网站实施。
Java 端介绍
Java 端作为后台调用 Android 本地 SDK 的接口,主要实现了如下的功能:
- 建立通讯机制,提供接口给浏览器端,方便 JavaScript 进行调用。
- 数据队列的维护,以保证浏览器端的调用后产生的数据可以回送。
- 插件体系的建立,提供整个框架的可扩展性。
而这三部分的功能对应到代码中则是如下的几个重要的 Java 类:
- DroidGap、CallbackServer
- CallbackServer
- Plugin、PluginManager
因此我们需要依次来了解这几个重要的 Java 类的具体实现,这样才可以对 PhoneGap 在 Android 上的体系有一个很好的了解。
DroidGap 的实现
当我们完成一个基本的 PhoneGap 的示例后,我们就会发现,在使用 PhoneGap 进行开发的手机应用中,第一步就是将继承关系 extends Activity 修改为 extends DroidGap。因此,DroidGap 是整个应用开始的地点,首先需要了解 DroidGap 的内容。
在源码中可以看到 DroidGap 继承自 PhonegapActivity,而 PhonegapActivity 是一个抽象类,继承自 Activity,但是具体的实现都是集中在 DroidGap 类中。
因此,我们继续回到 DroidGap 类中,当我们知道 DroidGap 类也是一个 Acitvity 后,就会明白,DroidGap 在 onCreate 方法中实现了整个类最初的一些操作,代码的实现则集中在 onCreate() 和 init() 这两个方法中,具体的流程如下:
- 设置 WebView
设置 WebView 的代码都集中在 init() 方法中,其中一个值得注意的工作是设置 WebChromClient。代码中提供了一个继承自 WebChromClient 的类,重写了其中的 onJsAlert,onJsConfirm,onJsPrompt 等方法。而在 onJsPrompt() 方法中,实现了 PhoneGap 中 Java 端和浏览器端通讯的关键一步。因此,值得我们专门的关注其代码,具体的功能实现代码如下,我们已经过滤了一些错误处理、安全验证等等可能会妨碍我们对重要的功能实现理解的代码:
代码清单 1. WebView 代码
if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) { JSONArray array; try { array = new JSONArray(defaultValue.substring(4)); String service = array.getString(0); String action = array.getString(1); String callbackId = array.getString(2); boolean async = array.getBoolean(3); String r = pluginManager.exec(service, action, callbackId, message, async); result.confirm(r); } catch (JSONException e) { e.printStackTrace(); } } // Polling for JavaScript messages else if ......
在此,我们就可以明白,实现 JavaScript 与 Java 端通讯的原理是 JavaScript 利用 prompt 来传递调用信息的数据,在 onJsPrompt 中,重写的方法截获了这些数据,在完成了对数据格式等等分析后,按照要求进行具体的调用。而具体的调用利用是的 PluginManager 类,这将在随后说明。
- 绑定 js
在完成 WebView 的设置后,再将 PhoneGap 和 js 绑定,实现 js 与 PhoneGap 的通讯。在这一步中主要的内容就是实例化了 CallbackServer 和 PluginManager 两个类。因此,更多的内容会在随后对这两个类的介绍中再说明。
- 载入 URL
LoadUrl 方法也是实现一个 PhoneGap 示例后很熟悉的方法,在 loadUrl 中,完成基本的字符串处理后,主要利用 runOnUiThread 开始处理,其中第一步检测是否需要提供载入的显示,之后使用 WebView 的 loadUrl 载入内容,并且设置超时时间。
至此,Java 端的准备工作也就完成,实现了 UI 的载入。可以看到,在 DroidGap 类中最重要的一个部分就是截获 JavaScript 的 prompt 数据。这是实现浏览器端和 Java 端通讯的基础。
Plugin 的实现
Plugin 是一个抽象类,实现了 IPlugin 接口,PhoneGap 中利用 Android SDK 实现逻辑代码的途径就是通过继承 Plugin 来实现。应此 Plugin 本身非常简单,最主要的部分是提供一个 execute 方法,如下:
public abstract PluginResult execute(String action, JSONArray args, String callbackId);
Plugin 的实现中的重要逻辑在其中来实现,它返回的是一个 PluginResult 对象,这个对象主要负责传递数据信息,包括的成员主要如下:
private final int status;
用于返回状态,status 与一个 enum 类型相关,用于标注插件的执行结果如何。
private final String message;
用于返回数据信息。
PluginManager 的实现
PluginManager 事实上是 DroidGap 类和具体的继承自 Plugin 的插件的联系纽带,由它来寻找和载入插件,并且调用。
首先,PluginManager 在构造函数中,调用了 loadPlugins 方法,该方法负责解析 xml 配置文件,对应每个 plugin,调用一次 addService 用于注册插件,具体的注册地点,是通过维护一个 HashMap<String, String> 来实现,分别保存 serviceType, className。在成员中,有另外一个 HashMap<String, IPlugin> 用于实现 className 和 IPlugin 类的绑定。这样就实现了 serviceType 和 IPlugin 的对应。
在 PluginManager 中,执行插件的方法 exec 是主要功能,我们将会详细说明一下这个方法的过程:
- 利用查询之前提到的 HashMap<String, String> 将 service 和 className 对应起来,这样我们就获得了提供功能的插件类的位置。
- 通过 getClassByName 来获得插件类。
- 完成类型检测,确定对应的类是一个插件类。
- 执行 addPlugin 方法,其中则将第 2 步中获得插件类的 className 和具体的 Plugin 接口的实现绑定,具体的实现就是上文中提到的 HashMap<Strig,IPlugin>。
- 判断 Plugin 是否需要异步执行,再根据情况,选择直接执行或者是建立新的线程来执行。
- 完成执行后,得到的数据会根据情况交给 CallbackServer 来处理,PluginManager 会调用 DroidGap 中的 sendJavaScript 来将数据交给 CallbackServer,事实上,DroidGap 中的 sendJavaScript 不过是对 CallbackServer 中的 sendJavaScript 包装,实际调用的是 CallbackServer 中的 sendJavaScript 方法,这涉及到了 CallbackServer 类,随后就会说明。
执行结果的转换则是利用 PluginResult 中的一些 to***String 方法。这些方法返回了一个 String 类型,其中包括了具体的 JSON 对象和 callbackId,具体的形式则是一个 js 代码,用于前端的调用。
CallbackServer 的实现
CallbackServer 实现了 Runnable 接口,具体的功能就是维护一个数据的队列,并且建立一个服务器,用于 XHR 的数据传递,对数据的队列的维护利用的是 LinkedList<String>。
由于实现的是 Runnable 接口,在 CallbackServer 中,最主要的方法就是 run() 方法,run() 方法的具体内容简介如下:
- 首先利用 ServerSocket 监听端口,具体端口则自由分配。
- 在 accept 后则是对 HTTP 协议的解析,和对应的返回 status code。
- 在验证正确后,利用 getJavascript 方法得到维护的 LinkedList<String>() 中的保存的 js 代码,如果为空则返回 null。
- 这些具体的 string 类型的 js 代码则利用 socket 作为 response 返回给前端。
之后就是对队列维护的方法,这时理解之前的 sendJavaScript 则很简单,该方法与 getJavaScript 相反,一个是从 LinkedList 中取出 js 代码,一个则是加入。
综上,CallbackServer 实现的是两个功能,一个是 XHR 的 SocketServer,一个是对队列的维护。而完成 CallbackServer 类的说明后,Java 端的主要功能也都说明完毕。随后会说明浏览器端的说明,在完成浏览器端说明后,我们就会明白整个 PhoneGap 框架运行的原理。
浏览器端介绍
浏览器端的 JavaScript 代码较多,但是实现其中核心功能的代码都在 PhoneGap 类中,其他的都是 PhoneGap 框架中自带的一些 Plugin 的 JavaScript 代码。
在浏览器端,其中一个最重要的函数就是 PhoneGap.exec 函数,因为大部分的函数调用,最终都会在这里实现与本地 Java 端的数据通讯,转而去调用 Java 端的 Plugin 来实现具体的执行。主要的内容如下,我们已经去除一些非功能的错误处理代码段,集中于具体的功能实现:
代码清单 2. PhonGap.exec 代码
var callbackId = service + PhoneGap.callbackId++; if (success || fail) { PhoneGap.callbacks[callbackId] = {success:success, fail:fail}; } var r = prompt(PhoneGap.stringify(args), "gap:"+ PhoneGap.stringify([service, action, callbackId, true])); // If a result was returned if (r.length > 0) { eval("var v="+r+";"); // If status is OK, then return value back to caller if (v.status === PhoneGap.callbackStatus.OK) { ...... }else if ...... }
在以上标红加粗的代码段上,我们可以看到,调用的是 prompt 方法 , 我们在之前特别提到了,PhoneGap 在 DroidGap 类中,继承了一个WebChromeClient
,其中重写了方法 onJsPrompt,这样在 Java 端,就截获了浏览器端的调用。现在可以回到之前的 Java 端的说明,查看之前列出的 onJsPrompt 代码,联系之前的 Java 端的介绍,就会觉得豁然开朗了,剩下了流程也就顺理成章了。通过 r 的返回值,就可以轻松的返回数据给浏览器端了。
这时我们会有一个好奇,那就是 CallbackServer 的 SocketServer 的作用在哪里,因为至此我们已经看到了整个数据在浏览器端和 Java 端的通讯,那么这样的一个 SocketServer 又是提供的什么数据通讯呢?
显然,在 PluginManager 中的 exec
中,我们会返回执行获得的数据,但是,在使用异步调用的时候,立即返回的则是一个空字符串,也即没有数据返回,之后插件会在一个新的线程中执行,等到异步的插件执行的结果返回数据后,会放入到队列中,等待 XHR 的获得。而 XHR 的调用是在 JavaScript 代码初始化中开始调用的,其中由于 JsCallBack 函数有使用轮询保证自己循环执行。这样就可以不断的获得异步数据。
Hello 插件的实现
掌握了上文中对 PhoneGap 框架的分析,我们就可以通过自己实现相关的插件来完成自己希望实现的功能,这样就可以不断丰富自己应用的功能。我们假设开发者已经熟悉了 PhoneGap 和 Android 的基本开发知识,Android 和 PhoneGap 开发环境搭建已经完成。
在具体的 Plugin 实现中,要分两个部分来实现,Java 代码和 JavaScript 代码,就此流程,我们来通过一个简单的例子来看一下如何具体的完成 Plugin 的实现。
在 Java 端,我们要继承一个 Plugin 类,实现其中的 execute 方法,在这里,我们省略具体的功能逻辑。
在 JavaScript 端,我们要利用现有 PhoneGap.exec 来调用具体的 Java 端,而且需要注意的是要在 PhoneGap 中注册这一个插件,PhoneGap.addConstructor
就在 JavaScript 端调用 Java 端代码,完成添加,当然直接修改 XML 文件来实现也可以。
首先,完成 Java 端的代码实现。
我们实现一个简单的字符串回射程序,来模拟具体的逻辑,代码如下:
清单 3. 插件实例 Java 端代码
@Override public PluginResult execute(String action, JSONArray args, String callbackId) { PluginResult.Status status = PluginResult.Status.OK; PluginResult r=null; if (action.equals("sayHello")){ r = new PluginResult(status,"Hello"); }else if (action.equals("saySth")){ r = new PluginResult(status,args); } return r; }
实现的功能如下:
- 如果 action 的参数是 sayHello,则返回字符串“Hello”。
- 如果是 saySth,则返回 args 中的内容。
之后,完成 JavaScript 代码
请注意 JavaScipt 代码是需要 HTML 代码包含后才能使用的:
清单 4. 插件实例 JavaScript 端代码
var Hello=function(){ } Hello.prototype.saySth = function(msg, callback, fail) { return PhoneGap.exec(function(args) { callback(args); }, function(args) { if(typeof fail == 'function') { fail(args); } }, 'Hello', 'saySth', [msg]); } Hello.prototype.sayHello = function(callback, fail) { return PhoneGap.exec(function(args) { callback(args); }, function(args) { if(typeof fail == 'function') { fail(args); } }, 'Hello', 'sayHello',['sayHello']); } PhoneGap.addConstructor(function() { PhoneGap.addPlugin('hello', new Hello()); //PluginManager.addService("Hello","qj.Hello"); })
在这里我们需要有一个特别的说明:
在上述 JavaScript 代码中,我们注释了 addConstructor 函数中的一行,因为我们在开发过程中发现这条代码的执行在 Javascript 端没有被实现。
因此这段代码具体的执行则需要我们在 Java 端实现代码,那么为了实现这段 JavaScript 代码所做的补偿性的 Java 代码要在自己实现的 Java 端的 onCreate 函数中实现,内容是很简单的一句(当然,也可以通过修改 xml 文件来实现这一功能,上文提到过,PluginManager 类会扫描 xml 文件来依次以对应 xml 文件中的插件名作为参数执行这一调用):
this.pluginManager.addService("Hello", "qj.Hello");
注意第二个参数是实现的插件的代码的位置,Hello 插件的位置是“src.qj.Hello.java”,对应的位置信息就是去除 .java 后缀名后的位置信息,整个项目的组织结构如图。
图 1. 项目组织结构图示例
至此,Hello 插件已经实现,如果需要调用,则 JavaScript 代码如下:
清单 5. 插件调用代码
window.plugins.hello.sayHello(function(arg){alert(arg);}, function(arg){alert(arg);}); window.plugins.hello.saySth("Bingo",function(arg){alert(arg);}, function(arg){alert(arg);});
我们简单的将返回的数据通过 alert() 展示出来,插件的执行结果如下:
点击 SayHello,
图 2. 点击 SayHello 示例
点击 SayBingo
图 3. 点击 SayBingo 示例
结束语
至此插件的开发已经完成,其中很大的篇幅都是通过对 PhoneGap 的框架的说明,让我们可以对插件的运行过程有一个更加底层的认识,我想这是比实现一个简单的插件更加重要的东西。如果读者需要了解更多的信息,请浏览参考资料中的开发者网站,获得更加实时的信息。