最近在做一个项目需要使用到H5混合开发,需要研究Cordova框架,看了其中的源码和实现方式,当场在看的时候马上能理解,但是事后再回去看相关源码时候却发现之前理解的内容又忘记了,又不得不重新开始看,所以总觉得需要记录下来,这样也表明之前也是学习过,俗话说「好记性不如烂笔头 」,想必也是体现了笔记的重要性。
为何要用Cordova
什么是Cordova
Cordova中UML类图
Cordova实现机制
小结
随着移动互联网的发展,现在基本是APP满天飞,不知在大家印象中,如果我去下载一个APP,那么基本都能看到有两种选择,一种是Android版本,一种是IOS版本。不管我的手机是哪种操作系统,安装完一个APP之后,后续如果有新的版本发布的时候,我还必须去更新,才能享用新版本里的功能,比如我装了“京东”这个APP,前几天正好碰到“618”活动,那么之前一个月APP Store就提醒我要去更新最新的APP版本,以免错过“618”活动中新的功能使用。相对来说IOS系统更新APP比起Android系统用户体验会好一点,但是还是稍显麻烦点。
那么有没有一种方式,我只需要开发一个APP版本,就能去适配通用的操作系统呢,不仅可以适配Android、IOS,还可以适配其他系统,比如Windows Phone、 Palm WebOS、Blackberry等等。有,Cordova就能提供这种能力,代码写一次,就能到处运行,跟我们日常开发网站效果一样,基于写Web APP,根据输出平台要求不同,就能提供不同类型的安装包。Cordova其设计初衷是希望用户群体能够通过跨平台开发的方法降低原生开发的成本,为此,开发人员需要安装原生开发环境,配置工程,使用HTML5、CSS3、JS和原生SDK生成应用。
官网定义如下:
Apache Cordova是一个开源的移动开发框架。允许你用标准的web技术-HTML5,CSS3和JavaScript做跨平台开发。 应用在每个平台的具体执行被封装了起来,并依靠符合标准的API绑定去访问每个设备的功能,比如说:传感器、数据、网络状态等。
使用Apache Cordova的人群:
移动应用开发者,想扩展一个应用的使用平台,而不通过每个平台的语言和工具集重新实现。
web开发者,想包装部署自己的web App将其分发到各个应用商店门户。
移动应用开发者,有兴趣混合原生应用组建和一个WebView(一个特别的浏览器窗口) 可以接触设备A级PI,或者你想开发一个原生和WebView组件之间的插件接口。
架构图
从图中,我们可以看到它提供了Web APP、WebView、Cordova Plugins。
Web APP
这是存放应用程序代码的地方,体现是你的具体业务逻辑模块。应用的实现是通过web页面,默认的本地文件名称是是index.html,这个本地文件应用CSS,JavaScript,图片,媒体文件和其他运行需要的资源。应用执行在原生应用包装的WebView中,这个原生应用是你分发到app stores中的。
WebView
Cordova启用的WebView可以给应用提供完整用户访问界面。在一些平台中,他也可以作为一个组件给大的、混合应用,这些应用混合和Webview和原生的应用组件。
Cordova Plugins
插件是Cordova生态系统的重要组成部分。他提供了Cordova和原生组件相互通信的接口并绑定到了标准的设备API上,这使你能够通过JavaScript调用原生代码。
其实Cordova通过命令来添加项目的,但是可以选择哪个平台去编译,比如我们添加Android平台,在Android默认mainActivity类,我们可以看到它其实继承CordovaActivity类,一切初始化条件是从loadUrl方法开始。
package com.example.hello;
import android.os.Bundle;
import org.apache.cordova.*;
public class MainActivity extends CordovaActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// enable Cordova apps to be started in the background
Bundle extras = getIntent().getExtras();
if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
moveTaskToBack(true);
}
// Set by in config.xml
loadUrl(launchUrl);
}
}
进而得到以下UML类图
简单分析下,CordovaActivity内依赖一个WebView类,一个Preferences类,一个CordovaInterface接口,并同时初始化一些配置信息。WebView具体实现是由CordovaWebViewImpl类,CordovaInterface接口具体实现是由CordovaInterfaceImpl类实现。
CordovaWebViewImpl是核心类,里面会把一些插件能力初始化,用一个PluginManager进行管理,包含一个引擎类—CordovaWebViewEngine,这个引擎是通过反射的方式创建,自身初始化的时候把NativeToJsMessageQueue关联起来,里面包含着以Js字符串为主的双向链表,把每次从前端通过JS代码存储起来,然后通过绑定的桥接方式Pop出到相应的Native代码中去。
最终实现由SystemWebViewEngine类来对Android系统中WebView控件进行二次包装,这个类的初始化是在CordovaWebViewImpl类反射创建,相关插件和消息传递也是通过SystemWebViewEngine进行绑定。
当Cordova框架启动时候,CordovaActivity类中的onCreate方法调用loadUrl方法即可启动,最终在SystemWebViewEngine类的init方法中,会调用webView的addJavascriptInterface方法,看到这个方法是不是很熟悉,我们常规让webView支持开启JavaScript调用接口也是使用此特性。
private static void exposeJsInterface(WebView webView, CordovaBridge bridge) {
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
LOG.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
// Bug being that Java Strings do not get converted to JS strings automatically.
// This isn't hard to work-around on the JS side, but it's easier to just
// use the prompt bridge instead.
return;
}
SystemExposedJsApi exposedJsApi = new SystemExposedJsApi(bridge);
webView.addJavascriptInterface(exposedJsApi, "_cordovaNative");
}
那么SystemExposedJsApi类new出来的对象就等同抛出“_cordovaNative”对象给JS端调用,进去看下SystemExposedJsApi类包含哪些内容,
class SystemExposedJsApi implements ExposedJsApi {
private final CordovaBridge bridge;
SystemExposedJsApi(CordovaBridge bridge) {
this.bridge = bridge;
}
@JavascriptInterface
public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
return bridge.jsExec(bridgeSecret, service, action, callbackId, arguments);
}
@JavascriptInterface
public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException {
bridge.jsSetNativeToJsBridgeMode(bridgeSecret, value);
}
@JavascriptInterface
public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException {
return bridge.jsRetrieveJsMessages(bridgeSecret, fromOnlineEvent);
}
}
其中最关键是exec方法,其中bridgeSecret代表选择哪个桥接方式,service一般对应着你本地Java文件类名,action代表java文件中方法名,callbackId代表回调函数的Id,也就是句柄,arguments代表传递的参数。看出其中设计思想了没,service往往是本地能力集的类名,比如web端想调用相机,一般起个Camera类代表这个相机服务类,然后在这个类中定义方法,也就是action参数,这个action名称可扩展,因为方法名称可各种各样,适合自定义功能扩展。
SystemExposedJsApi对象初始化
在创建SystemExposedJsApi时需要CordovaBridge类,CordovaBridge类初始化需要CordovaWebView的PluginManager对象和NativeToJsMessageQueue对象。因为所有的JS端与Android native代码交互都是通过SystemExposedJsApi对象的exec方法。在exec方法中执行PluginManager的exec方法,PluginManager去查找具体的Plugin并实例化然后再执行Plugin的execute方法,并根据同步标识判断是同步返回给JS消息还是异步。由NativeToJsMessageQueue统一管理返回给JS的消息。
何时加载Plugin,如何加载
Cordova中很重要的部分是插件,Cordova在启动每个Activity的时候都会将配置文件中的所有plugin加载到PluginManager,在第一次loadUrl方法时,就会去初始化PluginManager并加载plugin,PluginManager在加载plugin的时候并不是马上实例化plugin对象,而是只是将plugin的Class名字保存到一个hashmap中,用service名字作为key值。当JS端通过JavascriptInterface接口的SystemExposedJsApi对象请求Android时,PluginManager会从hashmap中查找到plugin,如果该plugin还未实例化,利用java反射机制实例化该plugin,并执行plugin的execute方法。
Cordova的数据返回
Cordova中通过exec()函数请求android插件,数据的返回可同步也可以异步于exec()函数的请求。在开发android插件的时候可以重写public boolean isSynch(String action)方法来决定是同步还是异步。Cordova在android端使用了一个队列(NativeToJsMessageQueue)来专门管理返回给JS的数据。
1,同步
Cordova在执行完exec()后,android会马上返回数据,但不一定就是该次请求的数据,可能是前面某次请求的数据;因为当exec()请求的插件是允许同步返回数据的情况下,Cordova也是从NativeToJsMessageQueue队列头pop头数据并返回。然后再根据callbackID反向查找某个JS请求,并将数据返回给该请求的success函数。
2,异步
Cordova在执行完exec()后并不会同步得到一个返回数据。Cordova在执行exec()的同时启动了一个XMLHttpRequest对象方式或者prompt()函数方式的循环函数来不停的去获取NativeToJsMessageQueue队列中的数据,并根据callbackID反向查找到相对应的JS请求,并将该数据交给success函数。
webView.sendJavascript 发送到js队列,onNativeToJsMessageAvailable 负责执行js.
Native 调用 JS 执行方式有三种实现 LoadUrlBridgeMode、 OnlineEventsBridgeMode、PrivateApiBridgeMode
1、webView.sendJavascript 发送js方法到JS队列
2、onJsPrompt 方法拦截,获取调用方式
3、调用setBridgeMode 方法调用onNativeToJsMessageAvailable 执行javascript调用
总的来说,使用Cordova框架开发优缺点很明显。
优点:
缺点:
最后想说一句,无论是选择原生模式开发还是Hybrid混合模式,一定是要基于具体业务场景去选择,而不是盲目和绝对化觉得哪种模式好就不做分析想当然的去选择,还是有选择的结合,要知道应用之美在于药到病除。