• Android PhoneGap简析


    上周研究了一下PhoneGap这个技术,主要是对它的API和插件机制的学习,总体来看这种技术还是有一定的市场,特别是对BS为主的开发来讲确实有很多吸引,当然,这种技术也有严重的短板,比如效率和调试。当然这些都是一个概括性的认识,今天打算在仔细的研究一下,把PhoneGap的jar包反编译了一下,在自己的疑问下,借助互联网的帮助,简单的分析了一下PhoneGap的内部实现和依靠的技术以及架构,再次总结一下。本人对Java和Javascript不太熟悉,里面不对的地方欢迎指正和交流。

    主要阐述内容:

    • WebView&JS通信
      PhoneGap技术可行性的前提基础
    • PhoneGap架构
      应用层和实现层两方面的架构简述,架构的实现核心以及其扩展性的稳定和成熟度
    • PhoneGap通信过程
      简述PhoneGap通信的过程,调用网页,执行JS,JS调用OS功能,同步异步回调系统返回值的完整过程
    • 其他
      结合地图应用的个人简单想法整理

    主要使用的资料和工具

    备注:在学习源码中有一些不太懂和疑惑的地方在博客中得到了很好的解释。文章中有部分文字图片粘贴自上面两个博客,这里为了文章的完整和连贯性,做了整理。

     

    WebView&JS通信

    在PhoneGap之前,先简单阐述一下Android下PhoneGap可行性的前提技术:WebView

    不同于PC平台下浏览器的多样化,在Mobile平台下,WebKit已经成为主流,在iOS和Android两大主要移动平台都已经比较成熟。WebKit包含一个网页引擎WebCore和一个脚本引擎JavaScriptCore,它们分别对应的是KDE的KHTML和KJS。不过, 随着JavaScript引擎的独立性越来越强,现在WebKit和WebCore已经基本上混用不分。在Android手机中内置了一款高性能webkit内核浏览器,在SDK中封装为一个叫做WebView组件。这样通过WebView组件和WebKit浏览器通信成为了可行。

    直接看代码,该代码实现了1WebView控件和网页的绑定2JS调用Java函数,实现JS调用系统函数的能力3Java调用JS,实现Java调用脚本的能力

    Java代码

       1: package com.webviewtest;
       2:  
       3: // 引用Android SDK,Handler、WebSetting、WebView
       4: import android.app.Activity;
       5: import android.os.Bundle;
       6: import android.os.Handler;
       7: import android.webkit.WebSettings;
       8: import android.webkit.WebView;
       9:  
      10: public class WebViewTestActivity extends Activity {
      11:     private WebView mWebView;       
      12:     private Handler mHandler = new Handler(); 
      13:     /** Called when the activity is first created. */
      14:     @Override
      15:     public void onCreate(Bundle savedInstanceState) {
      16:         super.onCreate(savedInstanceState);
      17:         setContentView(R.layout.main);
      18:         mWebView = (WebView) findViewById(R.id.webview);
      19:         WebSettings webSettings = mWebView.getSettings(); 
      20:         webSettings.setSavePassword(false); 
      21:         webSettings.setSaveFormData(false); 
      22:         // 保证WebView支持调用javascript脚本
      23:         webSettings.setJavaScriptEnabled(true); 
      24:         webSettings.setSupportZoom(true);
      25:         // 添加可供Javascript调用的接口mydata,该函数在此模拟调用系统函数,返回数值10,验证js调用Java的方法
      26:         mWebView.addJavascriptInterface(new Object() {       
      27:             public int mydata(){ 
      28:                 return 10; 
      29:             } 
      30:         }, "demo");  
      31:         /*
      32:         // 该接口验证Java调用js脚本的方法,调用demo页面下的js接口wave()
      33:         mWebView.addJavascriptInterface(new Object() {       
      34:             public void clickOnAndroid() {       
      35:                 mHandler.post(new Runnable() {       
      36:                     public void run() {       
      37:                         mWebView.loadUrl("javascript:wave()");       
      38:                     }       
      39:                 });       
      40:             }       
      41:         }, "demo");       
      42:          */
      43:         
      44:         // WebView控件绑定demo页面,实现Native和Web结合
      45:         mWebView.loadUrl("file:///android_asset/demo.html");  
      46:     }
      47:     
      48:     
      49: }

    简单解释一下,首先初始化要注意setJavaScriptEnabled函数,设置为True保证WebView支持调用JS脚本

    WebView::loadUrl接口:调用网页或js接口,实现Android Java(Native Code)调用Web

    WebView::addJavascriptInterface:增加javascript识别接口,实现js调用Android Java

    好了,通过简单的示例java实现思路大致如上,下面看一下demo页面的实现,则更为简单

    HTML代码

       1: <html>      
       2: <body> 
       3:     <input type="text" id="a" value="test"></input>
       4:     <a onClick=va()>       
       5:         Click me!       
       6:     </a>
       7:     <a onClick=window.demo.clickOnAndroid() >       
       8:         <img id="droid" src="01.png" /><br>       
       9:         Click me!       
      10:     </a>            
      11: </body>       
      12: </html>   

    JS代码

       1: <script>       
       2: function wave() {       
       3:     document.getElementById("droid").src="02.png";       
       4: }
       5:  
       6: function va()
       7: {
       8:     document.getElementById("a").value=window.demo.mydata();
       9: }
      10: </script> 

    HTML页面简单解释:第一个click事件:va调用Java接口demo.mydata(),返回10,修改text内容,验证js调用java;第二个事件:调用Javademo.clickOnAndroid,该函数调用javascript:wave(),从而验证Java调用js

    不用任何第三方jar包,在Android下粘贴此代码,可以很好的在真机验证,还有,demo.html页面直接粘贴在assets文件夹下即可

    PhoneGap架构简述

    接下来,我们来看一下PhoneGap的架构组成。

    PhoneGap在应用层上很简单清晰,将Native Code封装成13个类,通过上面的技术转化为可以调用的phonegap.js脚本,来供Web开发者在网页开发中应用。Phonegap提供的整体方案,下图阐述的非常好,盗用一下,一图胜千言啊,呵呵。

    而在内部实现的架构,其实也并不复杂,分析jar包,里面的class如下:

    分析一下PhoneGap类库架构,结合PhoneGap示例,首先我们最感兴趣的,就是DroidGap这个class,源码截取部分如下 :

       1: package com.phonegap;
       2:  
       3: public class DroidGap extends PhonegapActivity
       4:
       5:     protected WebView appView;
       6:     protected WebViewClient webViewClient;
       7:     protected PluginManager pluginManager;
       8:

    通过类结构结合WebView的了解,相信就如梦初醒般的释疑了吧,其实这厮也是在WebView的基础上进行了一把封装,就起神奇性来说,还是远远无法和这个国度来媲美滴。虽然截取不多,我们也能得到PhoneGap结构第一个结论:PhoneGap整体技术思路建立在WebView的基础上,是结合WebView、Native Code和HTML技术的中间层封装。

    接下来,我们打开Plugin和一个具体的Plugin类,截取代码如下,有兴趣自己写一个的可以参考如下链接,可以更好的理解:

       1: public abstract class Plugin
       2:   implements IPlugin
       3: {
       4:   public String id;
       5:   public abstract PluginResult execute(String paramString1, JSONArray paramJSONArray, String paramString2);
       6:
       7:  
       8: public class DirectoryListPlugin extends Plugin {  
       9:     /** List Action */  
      10:     public static final String ACTION = "list";  
      11:   
      12:     @Override  
      13:     public PluginResult execute(String action, JSONArray data, String callbackId) {  
      14:         return result;  
      15:     }  
      16:

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

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

       1: public final class PluginManager
       2: {
       3:     private HashMap<String, IPlugin> plugins = new HashMap();
       4:     private HashMap<String, String> services = new HashMap();
       5:  
       6:     public PluginManager(WebView app, PhonegapActivity ctx)
       7:     {
       8:         loadPlugins();
       9:     }
      10:     
      11:   public void loadPlugins()
      12:   {
      13:     int id = this.ctx.getResources().getIdentifier("plugins", "xml", this.ctx.getPackageName());
      14:     if (id == 0) pluginConfigurationMissing();
      15:     XmlResourceParser xml = this.ctx.getResources().getXml(id);
      16:     int eventType = -1;
      17:     while (eventType != 1) {
      18:       if (eventType == 2) {
      19:         String strNode = xml.getName();
      20:         if (strNode.equals("plugin")) {
      21:           String name = xml.getAttributeValue(null, "name");
      22:           String value = xml.getAttributeValue(null, "value");
      23:  
      24:           addService(name, value);
      25:         }
      26:       }
      27:       try {
      28:         eventType = xml.next();
      29:       } catch (XmlPullParserException e) {
      30:         e.printStackTrace();
      31:       } catch (IOException e) {
      32:         e.printStackTrace();
      33:       }
      34:     }
      35:   }
      36:  
      37:   public String exec(String service, String action, String callbackId, String jsonArgs, boolean async)
      38:   {
      39:   }
      40: }
       1: public class DroidGap extends PhonegapActivity
       2: {
       3:     public void init()
       4:     {
       5:         bindBrowser(this.appView);
       6:     }
       7:     
       8:     private void bindBrowser(WebView appView)
       9:     {
      10:         this.pluginManager = new PluginManager(appView, this);
      11:     }
      12: }
      13:     

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

    PhoneGap通信过程

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

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

       1: public class DroidGap extends PhonegapActivity
       2: {
       3:     protected WebView appView;
       4:     public void init()
       5:       {
       6:         this.appView = new WebView(this);
       7:         
       8:     
       9:         if (Build.VERSION.RELEASE.startsWith("1.")) {
      10:           this.appView.setWebChromeClient(new GapClient(this));
      11:         }
      12:         else {
      13:           this.appView.setWebChromeClient(new EclairClient(this));
      14:         }
      15:             
      16:         settings.setJavaScriptEnabled(true);
      17:             
      18:         String url = getStringProperty("url", null);
      19:         if (url != null) {
      20:           System.out.println("Loading initial URL=" + url);
      21:           loadUrl(url);
      22:         }
      23:       }
      24:     public void loadUrl(String url)
      25:     {
      26:         this.val$me.appView.loadUrl(this.val$url);
      27:     }
      28: }

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

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

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

    如上当脚本调用DirectoryListing.list,最终是调用的PhoneGap.exec函数来进行的具体实现,接下来要出场的是PhoneGap.js这个文件了,看实现:

       1: PhoneGap.exec = function(success, fail, service, action, args) {
       2:     try {
       3:         var callbackId = service + PhoneGap.callbackId++;
       4:         if (success || fail) {
       5:             PhoneGap.callbacks[callbackId] = {success:success, fail:fail};
       6:         }
       7:  
       8:         var r = prompt(PhoneGap.stringify(args), "gap:"+PhoneGap.stringify([service, action, callbackId, true]));
       9:     } catch (e2) {
      10:         console.log("Error: "+e2);
      11:     }
      12: };

    这个脚本其实我也不太理解,哎,没文化真可怕啊,只看见五个参数,两个callback回调,一个类指针,一个类函数指针(允许我用指针来说吧,顺口一点),一个用户参数,然后里面的核心函数就是皮肉,prompt()这时候网上给的资料让我理解了:WebChromeClient提供了一个onJsPrompt方法,这个方法是当web端调用prompt方法时就会调到。于是乎,它就把这个方法给改了,改成Android向Web端暴露的接口,当Web要调用任何Android(Java)端的方法时,就调prompt,onJsPrompt被调后,它再去解析参数来代理后续的行为。这时,它就主要是调用Plugin,通过Plugin来满足Web端的需求。时序图如下图所示:

     换句话说,WebKit的这个函数,接下来会调用onJsPrompt()函数,onJsPrompt()调用exec函数,那让我们来看一下他们又做了什么事情,依次代码如下:

       1: public class DroidGap extends PhonegapActivity
       2: {
       3:     public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
       4:     {
       5:         String r = DroidGap.this.pluginManager.exec(service, action, callbackId, message, async);
       6:     }
       7: }
       8:  
       9: public final class PluginManager
      10: {
      11:     public String exec(String service, String action, String callbackId, String jsonArgs, boolean async)
      12:     {
      13:         String clazz = (String)this.services.get(service);
      14:         if (isPhoneGapPlugin(c)) {
      15:             IPlugin plugin = addPlugin(clazz, c);
      16:             if (runAsync)
      17:         {
      18:           Thread thread = new Thread(new Runnable(plugin, action, args, callbackId, ctx)
      19:           {
      20:             public void run() {
      21:               try {
      22:                 PluginResult cr = this.val$plugin.execute(this.val$action, this.val$args, this.val$callbackId);
      23:                 int status = cr.getStatus();
      24:  
      25:                 if ((status != PluginResult.Status.NO_RESULT.ordinal()) || (!(cr.getKeepCallback())))
      26:                 {
      27:                   if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
      28:                     this.val$ctx.sendJavascript(cr.toSuccessCallbackString(this.val$callbackId));
      29:                   }
      30:                   else
      31:                   {
      32:                     this.val$ctx.sendJavascript(cr.toErrorCallbackString(this.val$callbackId)); }
      33:                 }
      34:               } catch (Exception e) {
      35:                 PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
      36:                 this.val$ctx.sendJavascript(cr.toErrorCallbackString(this.val$callbackId));
      37:               }
      38:             }
      39:           });
      40:           thread.start();
      41:           return "";
      42:         }
      43:  
      44:         cr = plugin.execute(action, args, callbackId);
      45:  
      46:         if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && (cr.getKeepCallback()))
      47:           return "";
      48:         }
      49:     }
      50: }

    上面的代码简单介绍如下,当你轻轻在界面点击一个按钮,触发js事件时,PhoneGap脚本调用prompt函数,这会促发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的内部实现机制有一个大概的了解,学以致用,如果从结果的角度来考虑,我们能对是否采用PhoneGap开发有一个简单的判断吧。

    首先要承认,PhoneGap这样的实现,自己封装了一大堆,确实在效率上是有损失的,这点没办法,如果直接调用WebView,小打小闹还没问题,如果真要有规模,没有设计是不行的,这让我想到了OpenGL,有了OpenGL为啥还有OGRE和OSG以及Unity3D,直接用OpenGL属于玩死自己的一种做法,呵呵。

    其次,你要评估一下,如果你自己做封装,你是否有足够的能力做到差不多的水平,坦白说,PhoneGap这套东西还是有积极成分的,特别随着移动平台和浏览器技术的发展,这种让用户专注页面开发的方式还是有一些人性化的

    然后,PhoneGap这种Plugin的方式确实具有通用性,无论什么系统函数,在这种应用下都可以封装,即使是C++的接口,你都可以通过JNI封装成Java后,在通过这种技术来进行展现,比如大数据,渲染,三维等高性能的核心,技术上都可以走通,但也有接口封装繁杂,对开发人员要求较高,调试等问题,另外,还需要将C++面向对象的封装成JNI面向过程的,然后在封装成Java面向过程,然后在封装成JS面向过程,然后再通过原型封装成面向对象的,呵呵,这下终于圆满了,这种体系产品不是随便一个公司能搞定的。

    最后,就是开发人员的积累,如果是BS开发丰富的,这种方式还是有很多积极成分的,专注Web技术,将产品做好,底层的效率,大数据这些问题如果可以接受,这种损失也是可行的,如果是CS开发,这种技术就有点过于先进了,毕竟本身这种技术归类于Web比较合适。要保证开发者用的顺手吧。

    当然还有产品本身的定位,技术是死的,人是活的,产品是给人用的,结合目前的现状,发挥自己的有点,从产品设计上避免自己的缺点,这就需要综合考虑。

    个人总结,PhoneGap这种方式很适合作为Web产品的App移植通用解决方案,除非你有足够的能力来搞定Native Code,除非你的产品对Native Code要求足够的简单,除非你不做App应用,否则在技术可行性上就没有更好的解决方案。

    另外感慨技术发展的太快了,这种快,有时候让人觉得兴奋,可以很轻松的做很多以前看似不可能的事情,有时候也让人觉得失落,因为有些发展或多或少有点肤浅,有种过眼云烟的味道。

  • 相关阅读:
    python之squid实现免费 IP代理 (windows win7 单机 本机 本地 正向代理 区分 HTTPS)
    python之PIL 二值图像处理和保存
    python之GIL release (I/O open(file) socket time.sleep)
    python之多线程 threading.Lock() 和 threading.RLock()
    python之GIL官方文档 global interpreter lock 全局解释器锁
    python多线程之t.setDaemon(True) 和 t.join()
    python之工作目录和文件引用
    python 代理
    原 浅谈移动端开发--物理像素和逻辑像素
    解决移动端页面在苹果端滑不到底部的问题
  • 原文地址:https://www.cnblogs.com/rongxiang/p/2752249.html
Copyright © 2020-2023  润新知