PhoneGap的架构组成

PhoneGap在应用层上很简单清晰,将Native Code封装成13个类(phonegap.jar或者cordova.jar),通过webview中java和js交互技术转化为js脚本(phonegap.js或者cordova.js),来供Web开发者在网页开发中应用。Phonegap提供的整体方案。

img/phonegap/phonegap架构图.gif

下面是几个关键点:

这点比较简单,大家都想得到,它实际上就是个内嵌的浏览器,各个移动平台也提供了这样的组件,在Android上就是WebView。

但Phonegap对WebView做了些改造,它通过扩展WebViewClient和WebChromeClient改变了些标准行为,它用CordovaWebViewClient扩展WebViewClient,并复写shouldOverrideUrlLoading、onPageStarted、onPageFinished等方法,使得它扩展了web纯url网页调用的行为,具备了通过geo:xxx调用intent,通过sms:xxx发短信等能力。另外,它用CordovaChromeClient扩展WebChromeClient,并复写了onJsAlert、onJsConfirm等方法,用Native的风格的窗口来相应js端alert、confirm的调用,使其更像是一个native的程序。更关键的是它复写了prompt,并通过这个方法来实现js对android端的调用,下面会详细谈这点。

总之,它就是基于CordovaWebViewClient和CordovaChromeClient扩展了WebView,使其具备标准的HTML执行渲染能力外,更具备Native化的样式和能力。这块的代码我就不具体去讲了,比较简单。

它的结构也比较扁平,总共三个类Plugin, PluginManager和PluginResult,一个配置文件plugin.xml。jar部分架构一节会有详细说明。

在phonegap通信过程一节有详细说明。

在phonegap通信过程一节有详细说明。

下图是另外一张phonegap设计概念图。

img/phonegap/phonegap概念.png

jar部分架构

内部实现的架构,其实也并不复杂,只需要分析jar包。分析一下PhoneGap类库架构,结合PhoneGap示例,首先我们最感兴趣的,就是DroidGap这个class,源码截取部分如下 :

package com.phonegap;
 
public class DroidGap extends PhonegapActivity
{
    protected WebView appView;
    protected WebViewClient webViewClient;
    protected PluginManager pluginManager;
}

通过类结构结合WebView的了解,其实这也是在WebView的基础上进行了一把封装。虽然截取不多,我们也能得到PhoneGap结构第一个结论: PhoneGap整体技术思路建立在WebView的基础上,是结合WebView、Native Code和HTML技术的中间层封装。

接下来,我们打开Plugin和一个具体的Plugin类,截取代码如下:


public abstract class Plugin
  implements IPlugin
{
  public String id;
  public abstract PluginResult execute(String paramString1, JSONArray paramJSONArray, String paramString2);
}
 
public class DirectoryListPlugin extends Plugin {  
    /** List Action */  
    public static final String ACTION = "list";  
  
    @Override  
    public PluginResult execute(String action, JSONArray data, String callbackId) {  
        return result;  
    }  
}

通过如上的Plugin抽象类和DirectoryListPlugin实体类,我们可以看到,PhoneGap采用插件化的方式来管理各个模块,这个你可以通过分析class可以验证,所有的模块封装,都是继承自Plugin,而纯虚函数execute则定义各个模块函数功能的规范,让具体的实体类来负责各自的实现。于是,我们得到了PhoneGap结构的第二个结论: PhoneGap是通过插件机制来管理自己的架构,从而将整个框架支撑在一起。 当然,这也是类库设计的一种经典方法了,及可以自用实现功能,也能扩展插件让用户自己开发。

下面,我们再看一下插件管理机制,主要是Plugin、PluginManager、PluginResult。,截取代码如下:

public final class PluginManager
{
    private HashMap<String, IPlugin> plugins = new HashMap();
    private HashMap<String, String> services = new HashMap();
 
    public PluginManager(WebView app, PhonegapActivity ctx)
    {
        loadPlugins();
    }
    
  public void loadPlugins()
  {
    int id = this.ctx.getResources().getIdentifier("plugins", "xml", this.ctx.getPackageName());
    if (id == 0) pluginConfigurationMissing();
    XmlResourceParser xml = this.ctx.getResources().getXml(id);
    int eventType = -1;
    while (eventType != 1) {
      if (eventType == 2) {
        String strNode = xml.getName();
        if (strNode.equals("plugin")) {
          String name = xml.getAttributeValue(null, "name");
          String value = xml.getAttributeValue(null, "value");
 
          addService(name, value);
        }
      }
      try {
        eventType = xml.next();
      } catch (XmlPullParserException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
 
  public String exec(String service, String action, String callbackId, String jsonArgs, boolean async)
  {
  }
}
public class DroidGap extends PhonegapActivity
{
    public void init()
    {
        bindBrowser(this.appView);
    }
    
    private void bindBrowser(WebView appView)
    {
        this.pluginManager = new PluginManager(appView, this);
    }
}

首先DroidGap类在初始化的时候会创建PluginManager类,可以简单认为DroidGap和PluginManager是一一对应的关系,全局变量(这种理解绝对上是不对的)。而PluginManager通过loadPlugins解析plugin.xml,将引用的插件加载进来,而在调用时则通过exec函数来具体到具体的Plugin插件来实现。于是,得到了PhoneGap架构的第三个结论: PhoneGap插件管理采用标准的工厂模式,通过xml进行解析和扩展,从而完成整个工作流的架构, 对了,忘记提PluginResult了,这个就是结果返回类的标准。

js部分架构

//todo

PhoneGap通信过程

好了,理解完上面的WebView思路和PhoneGap架构,我们就有一个好的基础来研究PhoneGap具体实现,比如一个完整的通信过程是如何实现,这就需要细化到具体的函数内部实现。

当我们写的第一个PhoneGap程序时,我想你的第一个代码肯定是这样的: super.loadUrl(file:///android_asset/www/index.html);。于是,在PhoneGap的帮助下,一个Hello World就展现在我们的眼前,这时候你就会眼前一亮,loadUrl,这个函数在WebView控件中也有,我们就来看一下PhoneGap的实现代码如下:

public class DroidGap extends PhonegapActivity
{
    protected WebView appView;
    public void init()
      {
        this.appView = new WebView(this);
        
    
        if (Build.VERSION.RELEASE.startsWith("1.")) {
          this.appView.setWebChromeClient(new GapClient(this));
        }
        else {
          this.appView.setWebChromeClient(new EclairClient(this));
        }
            
        settings.setJavaScriptEnabled(true);
            
        String url = getStringProperty("url", null);
        if (url != null) {
          System.out.println("Loading initial URL=" + url);
          loadUrl(url);
        }
      }
    public void loadUrl(String url)
    {
        this.val$me.appView.loadUrl(this.val$url);
    }
}

如上可见,DroidGap在初始化时调用WebView支持JS脚本解析,在loadUrl中,也是经过一些判断,最终也是调用WebView的loadUrl来实现最终功能。

下面就该具体的通信过程了,以DirectoryListPlugin插件的应用为例子,这个事PhoneGap自己提供的自定义插件开发的示例,用户在页面开发中代码如下:

<script type="text/javascript" src="phonegap.js"></script>  
<script type="text/javascript" src="directorylisting.js"></script>  
<script type="text/javascript" >
  document.addEventListener('deviceready', function () {
      var btn = document.getElementById("list-sdcard");
      btn.onclick = function () {
          DirectoryListing.list("/sdcard",
	  function (r) { printResult(r) },
	  function (e) { alert("error") }
	  );
      };
      btn.disabled = false;
  }, true);
</script>
// directorylisting.js
var DirectoryListing = {    
    list: function(directory,successCallback, failureCallback) {  
        return PhoneGap.exec(successCallback,        //Success callback from the plugin  
                             failureCallback,        //Error callback from the plugin  
                             'DirectoryListPlugin',  //Tell PhoneGap to run "DirectoryListingPlugin" Plugin  
                             'list',                 //Tell plugin, which action we want to perform  
                             [directory]);           //Passing list of args to the plugin  
    }  
}; 

当脚本调用DirectoryListing.list,是调用的PhoneGap.exec函数来进行的具体实现,该函数在PhoneGap.js这个文件中实现:

PhoneGap.exec = function(success, fail, service, action, args) {
    try {
        var callbackId = service + PhoneGap.callbackId++;
        if (success || fail) {
            PhoneGap.callbacks[callbackId] = {success:success, fail:fail};
        }
	
	//prompt()函数是重点,这个函数是属于js代码中的函数
        var r = prompt(PhoneGap.stringify(args), "gap:"+PhoneGap.stringify([service, action, callbackId, true]));
    } catch (e2) {
        console.log("Error: "+e2);
    }
};

prompt()是整个函数的重点。prompt()函数是如何调用到Android(Java)端的功能的呢?

WebChromeClient提供了一个onJsPrompt方法,这个方法是当web端调用prompt方法时就会调到。于是乎,它就把这个方法给改了,改成Android向Web端暴露的接口,当Web要调用任何Android(Java)端的方法时,就调prompt,onJsPrompt被调后,它再去解析参数来代理后续的行为。这时,它就主要是调用Plugin,通过Plugin来满足Web端的需求。时序图如下图所示:

img/phonegap/web2native时序图.gif

org.apache.cordova.CordovaChromeClient.onJsConfirm(WebView view, String url, String message, final JsResult result);这个方法会拦截html页面发送过来的Native Api请求(调用window.prompt()),然后交由对应的Plugin处理。接下来会调用onJsPrompt()函数,onJsPrompt()调用exec函数,那让我们来看一下他们又做了什么事情,依次代码如下:

public class DroidGap extends PhonegapActivity
{
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
    {
        String r = DroidGap.this.pluginManager.exec(service, action, callbackId, message, async);
    }
}
 
public final class PluginManager
{
    public String exec(String service, String action, String callbackId, String jsonArgs, boolean async)
    {
        String clazz = (String)this.services.get(service);
        if (isPhoneGapPlugin(c)) {
            IPlugin plugin = addPlugin(clazz, c);
            if (runAsync)
        {
          Thread thread = new Thread(new Runnable(plugin, action, args, callbackId, ctx)
          {
            public void run() {
              try {
                PluginResult cr = this.val$plugin.execute(this.val$action, this.val$args, this.val$callbackId);
                int status = cr.getStatus();
 
                if ((status != PluginResult.Status.NO_RESULT.ordinal()) || (!(cr.getKeepCallback())))
                {
                  if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
                    this.val$ctx.sendJavascript(cr.toSuccessCallbackString(this.val$callbackId));
                  }
                  else
                  {
                    this.val$ctx.sendJavascript(cr.toErrorCallbackString(this.val$callbackId)); }
                }
              } catch (Exception e) {
                PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
                this.val$ctx.sendJavascript(cr.toErrorCallbackString(this.val$callbackId));
              }
            }
          });
          thread.start();
          return "";
        }
 
        cr = plugin.execute(action, args, callbackId);
 
        if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && (cr.getKeepCallback()))
          return "";
        }
    }
}

上面的代码简单介绍如下,当你轻轻在界面点击一个按钮,触发js事件时,PhoneGap的js脚本调用prompt函数,这会促发java代码中DroidGap类的onJsPrompt,此函数最主要的作用是调用PluginManager的exec函数,这样将具体的js函数通过插件Manager来指定具体的Plugin来执行,这在PluginManager中通过Hash字典通过service参数获取具体的Plugin插件,专业的人做专业的事情嘛,这时候Plugin就通过execute来调用系统API,达到js与系统级别之间的通信。

另外,还有一个返回值问题的考虑,如果是同步,那很自然的通过返回的PluginResult来返回,而如果是异步方式,这就需要注意代码中Thread的应用,这时候PhoneGap创建了一个线程,来专门用于获取返回值,这个线程不停的Run,直到获取到返回的结果,然后以sendJavascript的方式回调给js,从而完成java通信js的过程。

另外,这个sendJavascript是干什么用的吗,呵呵,一看就是发送给Javascript的消息,这个一看就知道了,这里只是突然想到了MFC下面的经典面试题:PostMessage和SendMessage有什么不同,呵呵,SendMessage只是抛消息而不负责事件的返回就直接走人,而post则需要等待一个结果才负责人的闪人,说不定写sendJavascript接口这个人当时应该也是这么想的吧,可以通过是否有postJavascript这个接口来验证:)

这样,PhoneGap整个的过程都已经结束,其实总结下来其技术看起来确实很有新颖,但是本质看来并无什么惊叹之处,都是一些很实用直接的方法,当然这帮哥们应该是对Java客户端技术和HTML很熟悉的,但就代码而言还是很专业的,无论是质量还是设计。当然,其最难的部分确实是异步机制的处理,通过创建Thread的方式来实现,因为我对Java线程run运行机制不是很了解,直观来看这种方式在CS开发上没有问题,但是这样实现有点简单零散,感觉如果有观察者模式的方式来进行管理,代码上会更优雅一些,但本身返回值就是一个JSONArray,这样做是否有点小题大做,这也说不清楚,毕竟PhoneGap要考虑这么多移动平台的Native Code而非Android一个平台。

Phonegap架构之cs模型篇以cs模型的方式分析phonegap的架构,很有实践指导作用。