Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2195687
  • 博文数量: 866
  • 博客积分: 14125
  • 博客等级: 上将
  • 技术积分: 10638
  • 用 户 组: 普通用户
  • 注册时间: 2007-07-27 16:53
个人简介

https://github.com/landuochong

文章分类

全部博文(866)

文章存档

2019年(3)

2018年(1)

2017年(10)

2015年(3)

2014年(8)

2013年(3)

2012年(70)

2011年(103)

2010年(360)

2009年(283)

2008年(22)

分类: Java

2012-07-24 18:00:30

转自:http://sd6733531.iteye.com/blog/1511889

之前有一位前辈已经写了PhoneGap android源码的解析。但是,前辈写得比较简单,只是把通信原理提了一提。本篇源码解析,会对PhoneGap做一个全面的介绍。

关于Java/JS互调,鄙人接触也有一段时间了。在android sdk文档中,也有用JsInterfaceloadUrl做到交互的示例。但令我惊讶的是,PhoneGap并没有选择用JsInterface,而是使用拦截prompt这种hack做法。

PhoneGapandroid源码写得稍稍有点凌乱和啰嗦,后面会详细解析。好了,不废话了。开始正文了

一、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)。顺便多说一句,虽然execPhoneGap的一个关键模块,但由于受到平台差异影响,各个平台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. 如何处理同步回调和异步回调?

先说同步处理。从jspromptWebChromeClientonJSPrompt是一个跨线程的同步调用。图示如下




通过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并实现Pluginexecute方法。如下代码是一个极为简单的实现本地启动界面的插件。

 

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层在执行回调后将立即删除回调以释放资源。

其两个工具方法:toSuccessCallBackStringtoErrorCallbackString将生成一个JS回调语句。配合CallBackServer实现了NativeJS回调。

 

所有的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的异步执行模式。要想Pluginexecute在线程中执行,必须具备两个条件。其一是js“下旨”给PluginManager的时候表示希望异步调用。其二是Plugin自身是允许异步执行的。通过查看源代码,可以发现js端默认都是希望异步调用,因此是否开启异步模式将由PluginisSync决定。

PluginManager载入Plugin的方式其实非常简单。主要是通过读取plugins.xml中的配置。配置中的nameservice对应,valuePlugin的类路径对应。PluginManager载入Plugin是通过反射空构造器实现,因此需要特别注意自定义的Plugin不要有带参构造器。

PluginManagerPlugin的管理还包含广播生命周期以及广播消息的功能。其中生命周期方法onResume,onPause,onDestroy其实是和web页面生命周期密切相关的。(而不是Activity,注意与js层的onResume,onPause有很大区别!)这点从DroidGaploadUrlIntoView中可以看出。至于广播消息,则是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代理了PluginManagerpostMessage方法,此处实际是请求PluginManager向所有的Plugin广播网络切换的事件。如果其他的Plugin关心网络切换事件,只需要覆盖onMessage方法即可。这样就实现了Plugin插件之间的交互。

 

最后一块硬骨头是CallBackServer。代码行数其实一点也吓不倒人,短短400行而已。首先从轮询模式开讲,当载入的url不是本地页面时,由于受到跨域限制,将强制切换成轮询模式。注意getJavascriptsendJavascript这两个方法。

前面说过,CallBackServer是异步回调的基础。我们来看看轮询下的异步回调究竟是怎么玩儿的。

来看看BatteryListener插件,下面是它的execute方法

注意actionstart时候PluginResult的返回。它返回了NO_ResultkeepCallbackPluginResultexec.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包装成回调语句,并通过DroidGapCallBackServer 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请求后,将会调用CallBackServergetJavaScript并同步返回给pollingPolling接到回调后,通过eval便完成了js端回调代码的执行。

XHR的方式与轮询其实类似,js端的源码可以查看lib/android/plugins/android/callback.js

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




 

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

三、PhoneGapjs层源码

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


阅读(1341) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册