原文链接:http://labs.cross.hk/html/1706.html
这个项目是在2011年10月秘密进行的,不要问我为什么是秘密进行?问了我也不会告诉你,你懂的:-),在2012年的3月才做完,共2名工程师、1名视觉、1名交互设计、1名pd投入,从技术预演到项目开发完成,总计花了近5个月,看到这里你也许会觉得项目周期太长了吧?是的,下面我会细说。还有更让你匪夷所思的是这个版本的客户端最后没有发布,后面我也会细说。
phonegap是个什么东西?网上有很多资料,随便google一下便可知晓,我在这里再帮你复习一下,顺便自己也巩固一下phonegap的知识。
2008年的一次apple公司举办的ios开发者大会上,有几个来自Nitobi软件公司的Brock Whitten、Rob Ellis和Andre Charland等几位web工程师看到ios开发的复杂性,当时就突发奇想,能否做个东西降低ios开发的复杂性,这当时只是他们一拍脑袋的想法。很庆幸他们没有放弃这个突如其来的想法,后来他们继续讨论,再加上当时html5技术的火热,最后他们达成共识,确定做一个工具来弥补web和ios开发之间的不足,当时他们提出口号是:Bridging the gap between the web and the iPhone sdk。请注意,他们当时的口号并非是现在的:written once,run everywhere。写到这里我吐槽一把:有很多人在做一件事情时,一开始就想把它做的非常非常的大,恨不得自己做的工具可以影响到全球技术界!然而到最后连个屁也没有,而还有些人一开始想法极简单,甚至看上去可笑,再甚至你都想鄙视之,当这种人带着解决某个问题的热情去做一件事情时,这种事情的成功概率非常之大。生活中很许多许多这样的case,就拿我们身边的阿里巴巴、淘宝、支付宝来说无一不是,最之初就是为了解决一个小问题而生,然而到最后却成为一个参天大树,值得敬佩!在此祝愿phonegap越走越好!扯远了,我们再回到原来的思路,很快这帮哥们就把第一版的中间件写好了,放出来后,受到极大的欢迎和追捧,业界对它的反馈出乎他们的意料,这个时间应该是在09年,接着他们很快就推出了android adk和blackberry sdk,当这两个平台的sdk推出后,那简直是轰动整个屌丝界,所有的屌丝们都趋之若鹜的关注phonegap,说这是移动开发者的福音,是来拯救移动开发工程师的,就连全球技术大佬ibm也加入了phonegap的研发,此时的phonegap市场已经非常之大了,也有了自己的编译工具,如appMobi,不负众望,phonegap继续优化,继续成长,11年10月,整个Nitobi团队被adobe收购,随后adobe把phonegap送给了apache软件基金会,接着apache把phonegap改名为cordova,cordova是Nitobi团队当时坐落的街道名称,用此名来纪念Nitobi团队的贡献,在被adobe收购后,adobe为phonegap做了不少事情,最大的一件莫过于在adobe的dw中集成了phonegap开发环境,虽然不是很好用,但至少看出他们想把它做好,另外就是为Nitobi团队提供经济上的支持了,这个是必须的。
好了,咱们回忆了phonegap的历史,知道现在的phonegap叫cordova了,下面再来看看phonegap的架构吧,以下的内容有部分是摘录的,因为我并没有读多少phonegap android sdk的源码,另外请注意,以下的对phonegap架构的分析和理解都是基于android os的。
我们先来看看phonegap的基本架构:
我们只关注黄色部分,蓝色部分是mobile os那层,mobile os这层也好理解,就是调用mobile os开放出来的组件,拿android来说,有Activity、Service、ContentProvider、BroadCastReceiver这四大组件。黄色部分就是phongap的基本架构,由3大部分组成:webapp、webview、plugins。
1、webapp这层就是你的app,或者我们叫phonegap app,这很好理解,就是用html、css、javascript技术搭建的网页。当运行一个phonegap app时,用户看到的全是真实的网页,这些网页可以预先打包在客户端也可以放在远程服务器上,使用phonegap开发app,通常会选择预先打包,这也是phonegap的亮点之一。 phonegap支持预先打包
2、再来看左边第二个绿色部分,这个是webview,这是phonegap的核心,木有这个webview也就木有phonegap,更谈不是用web技术打造app。这个webview实际上就是个系统内置的浏览器,只是它没有ui部分,因此你看不到有地址栏、导航条、边框等ui元素,这个webview在android os中是一个webview实例(new WebView(this)),在ios中是一个UIWebView实例,这两个平台的webview实例用的都是webkit内核,blackberry、bada、webos、nokia也是,它是一个内置浏览器,但是它的功能绝非浏览器所能比的,当实例化一个webview组件时,就可以通过webview组件的各种方法对这个webview实例改造了,phonegap就利用这点对WebViewClient和WebChromeClient做了造造,用它自己的CordovaWebViewClient重写了WebViewClient的标准行为,这些标准行为是哪些呢?举个例子:alert、confirm,domReady等,替换浏览器原生alert、confirm的样式和domready的行为,还重写了其它的几个主要的方法:shouldOverrideUrlLoading、onPageStarted、onPageFinished,通过重写这3个方法,phonegap实现了url拦截、实现了在js中通过geo:/sms:等调用android os的intent。还有一个非常重要的是phonegap把window.prompt重写了,通过这个prompt实现了js与android os的通信,下面会单独提出来说这一块。 总之phonegap对系统层的webview组件做了一次封装或者说改造,使html网页能够达到native的样式和处理能力
3、最后我们看看右侧绿色phonegap plugins。用plugin模式来构建一个系统或者说框架是业界公认的靠谱做法,phonegap也不例外,它通过plugin、pluginmanager、pluginresult三个plugin类封装os的api并且用其来管理用户自定义的plugin,每个plugin也可以看作一个独立的模块。这个plugin系统与webview实例是扁平结构,plugin系统中的所有plugin也是扁平化的设计,不存在相互依赖。
我们看一下plugin超类:
/** * Plugins must extend this class and override one of the execute methods. */ public class CordovaPlugin { public String id; public CordovaWebView webView; // WebView object public CordovaInterface cordova; /** * @param cordova The context of the main Activity. * @param webView The associated CordovaWebView. */ public void initialize(CordovaInterface cordova, CordovaWebView webView) { assert this.cordova == null; this.cordova = cordova; this.webView = webView; } public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { CordovaArgs cordovaArgs = new CordovaArgs(args); return execute(action, cordovaArgs, callbackContext); } }
每个phonegap的plugin必须继承CordovaPlugin超类,然后由CordovaPluginManager管理,每个plugin都有自己的execute,各尽其责,弱耦合,职责单一这也是许多框架的设计原则,并且开放接口,允许用户通过继承CordovaPlugin超类实现自己的plugin。通过分析plugin系统的设计我们不难看出,phonegap整个架构中,核心功能全都由pluginmanager管理、支持。
我们再来看看PluginManager的load方法:
public void loadPlugins() { while (eventType != XmlResourceParser.END_DOCUMENT) { if (eventType == XmlResourceParser.START_TAG) { String strNode = xml.getName(); //This is for the old scheme if (strNode.equals("plugin")) { service = xml.getAttributeValue(null, "name"); pluginClass = xml.getAttributeValue(null, "value"); // System.out.println("Plugin: "+name+" => "+value); onload = "true".equals(xml.getAttributeValue(null, "onload")); entry = new PluginEntry(service, pluginClass, onload); this.addService(entry); } //What is this? else if (strNode.equals("url-filter")) { this.urlMap.put(xml.getAttributeValue(null, "value"), service); } else if (strNode.equals("feature")) { insideFeature = true; //Check for supported feature sets (Accelerometer, Geolocation, etc) //Set the bit for reading params String uri = xml.getAttributeValue(null,"name"); } else if(strNode.equals("param")) { if(insideFeature) { paramType = xml.getAttributeValue(null, "name"); if(paramType.equals("service")) service = xml.getAttributeValue(null, "value"); else if(paramType.equals("package")) pluginClass = xml.getAttributeValue(null, "value"); if(service.length() > 0 && pluginClass.length() > 0) { onload = "true".equals(xml.getAttributeValue(null, "onload")); entry = new PluginEntry(service, pluginClass, onload); this.addService(entry); service = ""; pluginClass = ""; } } } } }
在PluginManager的loadPlugin方法中,可以看到phonegap从plugin.xml中加载外部plugin,加载成功后便new一个plugin实体,这就是一个标准的工厂模式,这一模式也使PluginManager能够更方便的管理Plugins。
ok,咱们了解phonegap的基础架构后,再来看看phonegap是如何处理js和native的通信的,前面提到了phonegap重写了webkit的window.prompt实现了js与native的通信,怎么就能通过prompt实现了js和native的通信了呢?
你肯定很熟悉这行代码:
super.loadUrl(file:///android_asset/www/index.html);
当Dalvik运行到这里时,phonegap便实例化一个webview,并且调用setJavascriptEnabled(true),启用当前webview实例的javascript功能。这里的webview是CordovaWebView组件(也就是phonegap对原生webview组件的封装)。
再看下官方Camera例子:
// Retrieve image file location from specified source navigator.camera.getPicture(onPhotoURISuccess, onFail, { quality: 50, destinationType: destinationType.FILE_URI, sourceType: source } );
当你运行上面的代码时,phonegap将为你打开照机拍照片。看一下phonegap是怎么为你打开camera的。
这是cordova.js中Camera模块里的getPicture源码:
cameraExport.getPicture = function(successCallback, errorCallback, options) { argscheck.checkArgs('fFO', 'Camera.getPicture', arguments); options = options || {}; var getValue = argscheck.getValue; var quality = getValue(options.quality, 50); var destinationType = getValue(options.destinationType, Camera.DestinationType.FILE_URI); var sourceType = getValue(options.sourceType, Camera.PictureSourceType.CAMERA); var targetWidth = getValue(options.targetWidth, -1); var targetHeight = getValue(options.targetHeight, -1); var encodingType = getValue(options.encodingType, Camera.EncodingType.JPEG); var mediaType = getValue(options.mediaType, Camera.MediaType.PICTURE); var allowEdit = !!options.allowEdit; var correctOrientation = !!options.correctOrientation; var saveToPhotoAlbum = !!options.saveToPhotoAlbum; var popoverOptions = getValue(options.popoverOptions, null); var args = [quality, destinationType, sourceType, targetWidth, targetHeight, encodingType, mediaType, allowEdit, correctOrientation, saveToPhotoAlbum, popoverOptions]; exec(successCallback, errorCallback, "Camera", "takePicture", args); return new CameraPopoverHandle(); }; //exec module.exports = { exec: function(service, action, callbackId, argsJson) { return prompt(argsJson, 'gap:'+JSON.stringify([service, action, callbackId])); }, setNativeToJsBridgeMode: function(value) { prompt(value, 'gap_bridge_mode:'); }, retrieveJsMessages: function() { return prompt('', 'gap_poll:'); } };
从源代码中很清晰的看见,Phonegap将任务交给了exec函数,而exec中调用了webview的prompt函数并且把当前的Plugin名:Camera带上,此时程序会进入Camera Plugin,直接唤起照相机。
prompt是个非常重要的函数,WebView提供了一个OnJsPrompt的方法,这个方法在web调用prompt方法时会被调用,phonegap这帮哥们把它重写成os暴露给web的接口,当在web端调用prompt时,会调用OnJsPrompt方法,OnJsPrompt再分析传入的参数,找到需要调用的plugin,把具体的任务再交给plugin处理,OnJsPrompt更像是一个代理,ok这些都好理解,现在工作流到了具体的plugin了,在此不知你是否发现一个问题:处理结果,也就是返回值怎么拿?如果是同步调用的话,那直接通过PluginResult就可以拿到插件的处理结果,但同步带来的影响谁用谁知道,那如果异步调用,该怎么处理呢?如果你看了PluginManager源码,你会发现phonegap用了Thread处理异步调用的问题,当Phonegap接受到一个异步任务时,它会新开个线程,这个线程肯定不是主线程UI Thread,这个线程不停的run,直到获取plugin的返回结果后,然后通过sendJavascript方法返回给js,至此js和os的一次通信任务完成。phonegap的基础架构及工作原理就是介个样子的,再深入研究下去还有很多的知识可以挖掘,比如异步的处理、对webview的封装等,后面有时间再研究吧。
带着这些知识,我们再回头分析phonegap加载一个页面都做了哪些事情?当你super.loadUrl(“file:///XXX”)加载一个file协议的页面时,phonegap把任务交给CordovaWebView,这个CordovaWebView在Dalvik运行时先把WebView组件改掉,然后再调用webview.loadUrl,很明显多了一步修改webview组件的事情,就这一步带了明显的性能问题,导致phoegap的loadUrl慢于原生webview的loadUrl至少150毫秒左右,这个数据是我当时用G1手机测试发现的,我木有仔细看phonegap是怎么改造webview组件的而导致出现了这100多毫秒的性能问题,本来android原生浏览器性能就已经够蜗牛的了,用了phonegap就更蜗牛了!
我们再列举一个场景看看phonegap的性能问题,比如点击一个按钮打开照相机,phonegap收到这个任务后,把会任务交给pluginmanager,然后pluginmanager再把任务分配给camera,同时建立一个新线程不停的跑,直接camera返回数据后再停下来把拿到的数据再返回给js,这和web开发中的轮询好像是一回事嘛,这肯定有性能问题嘛,为毛不用观察者来做呢?我不知道phonegap团队为何用轮询处理异步调用,也许不在其位,不知其苦吧!
phonegap的性能问题是一直都存在的,众屌丝皆知,这也给了phonegap团队很大的压力和挑战,至今也没有看到他们在性能调优方面有很明显的进步,不过我在这里要为phonegap圆下场,性能问题是存在的,但有些性能问题并不是phonegap应该解决的,许多的性能问题都是系统层面的,大家不要一味的指责phonegap的性能很差!有些机器就算你不用phonegap,你的app也一样慢!难道不是么?phonegap能做的就是不断的优化自己的代码和性能问题(他们正在做),其它的我觉得完全可以忽略(甭管那些屌丝怎么指责),大家多看看phonegap为你我带来的好处,比如phonegap的跨平台解决方案,难道这不比你苦逼的一个平台一个平台的开发强多了,phonegap允许你使用web技术开发app,难道这不比你用xml绘制activity爽多了,维护成本更低了!大家不要人云亦云呀
文章开头说过我们做的Phonegap版支付宝最终也没有发布,主要原因也是因为性能太差,另外一部分原因是内部政策,新来的老板不看好杂交模式的app,因此我们就把已经进入测试状态的phonegap客户端给毙了!好可惜!好遗憾!早提测10天说不定我们的劳动成果就发掉了,唉,太尼玛伤心了…..
最后吐槽一下我觉得phonegap比较适合的场景和产品:
- 不追求高端用户体验的产品。这一点有些矛盾,你肯定会想:谁家的app不追求极至的用户体验?我们暂且不讨论产品体验,反正这是我的观点,也反正肯定有些产品是不需要很好的用户体验的,比如那些偏重业务的产品,如o2o产品等
- 适合创业团队。很多创业团队急于推出产品,可以考虑使用phonegap快速部署app上线,后期再慢慢雕琢。
- 适合做工具类产品
- 适合非核心业务。app中的某些不重要的业务可以考虑使用phonegap,比如新的支付宝客户端(钱包)中的券市场那部分业务就使用了phonegap
最后提醒:请根据你的产品形态和特性谨慎使用phonegap,总之不建议全客户端都使用phonegap