`
sd6733531
  • 浏览: 66523 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

android PhoneGap源码详解

阅读更多

传送门:

PhoneGap源码详解一

PhoneGap源码详解二

PhoneGap源码详解三

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

 

  • 大小: 45.3 KB
  • 大小: 47.4 KB
  • 大小: 49.8 KB
  • 大小: 7.3 KB
分享到:
评论
6 楼 异域的梅 2013-01-06  
分析的很消耗,先学习着,不懂再问
5 楼 wmyzcs 2012-07-28  
源码下载地址是什么啊
4 楼 sd6733531 2012-05-07  
华天下 写道
我想清楚
sd6733531 写道
华天下 写道
少年? 你弄清楚为什么会有xhr和poll两种方式存在了吗? 单用poll我觉得是可以解决问题的。xhr显得有点打酱油,难道是用来避免用poll方式的时候,频繁调用onjsprompt造成的性能问题?



大哥的猜测我十分认同。xhr很可能是考虑性能比polling好



我想清楚了,由于javascript无法多线程,所以不断用polling,会造成其它插件执行时候有阻塞, 因为其它插件调用本地的时候跟polling用的是同一个入口。


大哥所言甚是。正解!待过两天码字将javascript层源码解析放上来后,欢迎再来讨论!
3 楼 华天下 2012-05-07  
我想清楚
sd6733531 写道
华天下 写道
少年? 你弄清楚为什么会有xhr和poll两种方式存在了吗? 单用poll我觉得是可以解决问题的。xhr显得有点打酱油,难道是用来避免用poll方式的时候,频繁调用onjsprompt造成的性能问题?



大哥的猜测我十分认同。xhr很可能是考虑性能比polling好



我想清楚了,由于javascript无法多线程,所以不断用polling,会造成其它插件执行时候有阻塞, 因为其它插件调用本地的时候跟polling用的是同一个入口。
2 楼 sd6733531 2012-05-07  
华天下 写道
少年? 你弄清楚为什么会有xhr和poll两种方式存在了吗? 单用poll我觉得是可以解决问题的。xhr显得有点打酱油,难道是用来避免用poll方式的时候,频繁调用onjsprompt造成的性能问题?



大哥的猜测我十分认同。xhr很可能是考虑性能比polling好
1 楼 华天下 2012-05-05  
少年? 你弄清楚为什么会有xhr和poll两种方式存在了吗? 单用poll我觉得是可以解决问题的。xhr显得有点打酱油,难道是用来避免用poll方式的时候,频繁调用onjsprompt造成的性能问题?

相关推荐

    android phonegap源码详解(二)

    在本篇《Android PhoneGap源码详解(二)》中,我们将深入探讨PhoneGap这一混合移动应用开发框架的内部机制,以及如何利用它来构建原生功能丰富的Android应用。PhoneGap是一个基于Apache Cordova的开源框架,允许...

    android+phonegap+jquery mobile

    【标题】:“Android + PhoneGap + jQuery Mobile” 这个项目标题揭示了一个使用三种技术栈构建的移动应用程序:Android、PhoneGap和jQuery Mobile。Android是Google主导的开源操作系统,主要用于智能手机和平板...

    android PhoneGap 入门

    **Android PhoneGap 入门详解** PhoneGap 是一个开源框架,它允许开发者使用 HTML、CSS 和 JavaScript 这些Web技术来构建原生的移动应用程序。它基于 Apache Cordova 平台,通过桥接机制,将JavaScript 与移动设备...

    phonegap-splashscreen

    PhoneGap Splashscreen 知识点详解 PhoneGap 是一个开源框架,它允许开发者使用HTML、CSS和JavaScript构建原生移动应用程序。PhoneGap Splashscreen 模块则是 PhoneGap 的一个重要组件,用于在应用启动时显示一个...

    初试PhoneGap开发框架

    ### 初试PhoneGap开发框架知识点详解 #### 一、概览 本篇文章将通过一个实战案例介绍如何使用PhoneGap开发框架来构建跨平台移动应用。PhoneGap是一款开放源码的移动开发工具,允许开发者利用HTML、CSS以及...

    phonegap backup

    - **plugins**:这个文件夹可能包含了已安装的PhoneGap插件,每个插件有自己的目录,包含必要的源码和配置文件,用于扩展PhoneGap的功能。 3. **开发流程**: - **安装和配置环境**:开发者需要安装PhoneGap/...

    phonegap-plugin

    PhoneGap 插件开发详解 PhoneGap 是一个开源框架,它允许开发者使用 HTML、CSS 和 JavaScript 来构建原生移动应用程序。PhoneGap 的核心理念是通过 Web 技术来实现跨平台的移动应用开发,同时利用各平台的原生功能...

    Android_photogap

    总之,Android Photogap是利用PhoneGap框架开发的Android应用实例,通过理解和学习其源码,开发者可以更好地掌握混合应用开发技术,结合Web的便捷性和Android的广泛设备支持,高效地创建跨平台的应用程序。

    中科院计算所Android开发技术培训大纲.doc

    - 操作系统和开放联盟:讨论PhoneGap与Android操作系统的关系,以及其在开放源码社区的角色。 - 接口和工具:学习使用PhoneGap所需的开发环境和工具,如Android SDK等。 2. **HTML5 API和Event事件** - HTML5...

    CordovaTest:通过Cordova默认案例生成的Android项目源码-源码通

    **CordovaTest项目详解** **一、Cordova简介** Cordova,原名为PhoneGap,是一款开源的移动应用开发框架,允许开发者使用HTML5、CSS3和JavaScript来构建跨平台的移动应用程序。它将Web应用程序封装在原生的移动...

    Calendar-for-Android

    《构建Android日历插件:Calendar-for-Android详解》 在移动应用开发中,与用户交互的组件至关重要,其中日历功能就是一个常见的需求。本文将深入探讨名为"Calendar-for-Android"的Cordova插件,它允许开发者在...

    CalculadoraJSAndroid:使用JavaScript为Android应用程序开发的计算器

    **标题解析:** "CalculadoraJSAndroid"是一个项目名称,暗示了我们正在讨论一个使用JavaScript构建的Android应用程序,它...通过深入研究项目源码,开发者可以学习到如何在Android平台上利用JavaScript实现复杂功能。

    二开苹果cms视频网站源码模板_可封装双端app等.txt

    ### 二开苹果CMS视频网站源码模板及封装双端APP技术详解 #### 一、苹果CMS概述 苹果CMS(Apple Content Management System)是一款广泛应用于搭建视频网站的开源内容管理系统。它以其简单易用、功能强大著称,在...

    SignalStrength

    **SignalStrength 插件详解** 在移动应用开发中,了解设备的信号强度是十分重要的,尤其是在涉及通信质量或网络性能优化的情景下。SignalStrength 插件是针对 PhoneGap 平台的一个实用工具,它允许开发者获取设备的...

    TestCordova.zip

    《CorDova技术详解:构建跨平台H5与Native交互桥梁》 在移动应用开发领域,跨平台成为了不可或缺的需求。CorDova,作为一款强大的开源框架,为开发者提供了实现HTML5(H5)、JavaScript和CSS3等Web技术构建原生移动...

    QuizApp:电话应用程序是一个测验。 使用的技术

    PhoneGap将这些Web应用封装在原生的移动应用壳中,使得它们可以在各大移动操作系统(如iOS、Android和Windows Phone)上运行,提供了一种跨平台的开发方式。QuizApp使用PhoneGap,意味着它的代码base是基于Web的,但...

    ToastyPlugin:cordova插件

    **ToastyPlugin:Cordova插件详解** Cordova是一款开源的移动应用开发框架,它允许开发者使用HTML、CSS和JavaScript来构建原生的移动应用程序。Cordova通过将这些Web技术与设备APIs相结合,使开发者能够访问手机...

Global site tag (gtag.js) - Google Analytics