android PhoneGap源码详解
来源:未知 责任编辑:责任编辑 发表时间:2013-08-27 15:52 点击:次
android PhoneGap源码详解
androidphonegapcordova源码
传送门:
PhoneGap源码详解一
PhoneGap源码详解二
PhoneGap源码详解三
讨论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
的实现方式并不相同。
- 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.sendcr.toSuccessCallbackString(callbackId)); < /span>
- }
- // If error
- else {
- ctx.sendcr.toErrorCallbackString(callbackId)); < /span>
- }
- } catch (Exception e) {
- PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
- ctx.sendcr.toErrorCallbackString(callbackId)); < /span>
- }
- }
- });
- 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.sendcr.toErrorCallbackString(callbackId)); < /span>
- }
- 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
相关新闻>>
最新推荐更多>>>
- 发表评论
-
- 最新评论 更多>>