PhoneGap能实现跨平台,并且拥有强大的跨平台访问设备接口的能力,无非就是通过大家都有的WebView组件,实现了HTML5+CSS3+JS的解析,这也是跨平台移动开发相对于原生开发最大的优势,一套代码,大部分平台共用~
若抛弃其原理,那么在我们的开发中,JS与Java之间的调用方式(插件开发)可参考这篇文章PhoneGap插件开发 js与Java之间的交互例子 详解
那么,PhoneGap的底层框架原理究竟是什么样的呢?下面我们就来一起探讨一下~~
我们先来看看几个PhoneGap的核心类:
CordovaActivity:CordovaActivity入口,实现PluginManager、WebView的相关初始化工作,我们只需要继承CordovaActivity来实现自己的业务需求。
PluginManager: PhoneGap插件管理器。
ExposedJsApi :JS调用Native, 通过插件管理器PluginManager加载config.xml配置,然后根据service找到具体实现类。
NativeToJsMessageQueue:Native调用JS,主要包括三种方式:loadUrl()、轮询、反射WebViewCore来执行JS。
首先,定位到org.apache.cordova.CordovaActivity这个类,其实在2.9.1版本里面,DroidGap是继承CordovaActivity的,但是DroidGap类是空的,也就是我们如果继承了DroidGap,实际上就是直接继承了CordovaActivity...
public class CordovaActivity extends Activity implements CordovaInterface {
protected CordovaWebView appView;
protected CordovaWebViewClient webViewClient;
... ...
public void onCreate(Bundle savedInstanceState) {}
}
CordovaActivity继承Activity,重写了其onCreate()、onPause()、onResume()、onDestroy() 等方法,并实现了CordovaInterface接口,主要提供了PhoneGap插件与Activity的交互。在CordovaActivity里面,有一个加载URL的函数,我们来看一下
/**
* Load the url into the webview.
*
* @param url
*/
public void loadUrl(String url) {
// 初始化WebView
if (this.appView == null) {
this.init();
}
this.backgroundColor = this.getIntegerProperty("BackgroundColor", Color.BLACK);
this.root.setBackgroundColor(this.backgroundColor);
// If keepRunning
this.keepRunning = this.getBooleanProperty("KeepRunning", true);
// Then load the spinner
this.loadSpinner();
this.appView.loadUrl(url);
}
/**
* Create and initialize web container with default web view objects.
*/
public void init() {
CordovaWebView webView = new CordovaWebView(CordovaActivity.this);
CordovaWebViewClient webViewClient;
if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB)
{
webViewClient = new CordovaWebViewClient(this, webView);
}
else
{
webViewClient = new IceCreamCordovaWebViewClient(this, webView);
}
this.init(webView, webViewClient, new CordovaChromeClient(this, webView));
}
/**
* Initialize web container with web view objects.
*
* @param webView
* @param webViewClient
* @param webChromeClient
*/
@SuppressLint("NewApi")
public void init(CordovaWebView webView, CordovaWebViewClient webViewClient, CordovaChromeClient webChromeClient) {
LOG.d(TAG, "CordovaActivity.init()");
// Set up web container
this.appView = webView;
this.appView.setId(100);
this.appView.setWebViewClient(webViewClient);
this.appView.setWebChromeClient(webChromeClient);
webViewClient.setWebView(this.appView);
webChromeClient.setWebView(this.appView);
this.appView.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
1.0F));
if (this.getBooleanProperty("DisallowOverscroll", false)) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD) {
this.appView.setOverScrollMode(CordovaWebView.OVER_SCROLL_NEVER);
}
}
// Add web view but make it invisible while loading URL
this.appView.setVisibility(View.INVISIBLE);
this.root.addView(this.appView);
setContentView(this.root);
// Clear cancel flag
this.cancelLoadUrl = false;
}
这个loadUrl(String url)实际上就是在我们CordovaActivity子类中调用的super.loadUrl("file:///android_asset/www/index.html"); 然后,init()函数实现了CordovaWebView的初始化,其中还涉及到了CordovaWebViewClient以及CordovaChromeClient这两个类,CordovaWebViewClient继承了WebViewClient,CordovaChromeClient继承了WebChromeClient。
首先,我们来看一下CordovaWebView的构造函数
public class CordovaWebView extends WebView {
public CordovaWebView(Context context) {
super(context);
if (CordovaInterface.class.isInstance(context))
{
this.cordova = (CordovaInterface) context;
}
else
{
Log.d(TAG, "Your activity must implement CordovaInterface to work");
}
this.loadConfiguration();
this.setup(); //初始化WebView配置信息。
}
/**
* Initialize webview.
*/
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private void setup() {
this.setInitialScale(0);
this.setVerticalScrollBarEnabled(false);
if (shouldRequestFocusOnInit()) {
this.requestFocusFromTouch();
}
// Enable JavaScript
WebSettings settings = this.getSettings();
settings.setJavaScriptEnabled(true);
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
// Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2)
try {
Method gingerbread_getMethod = WebSettings.class.getMethod("setNavDump", new Class[] { boolean.class });
String manufacturer = android.os.Build.MANUFACTURER;
Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer);
if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB &&
android.os.Build.MANUFACTURER.contains("HTC"))
{
gingerbread_getMethod.invoke(settings, true);
}
} catch (NoSuchMethodException e) {
Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8");
} catch (IllegalArgumentException e) {
Log.d(TAG, "Doing the NavDump failed with bad arguments");
} catch (IllegalAccessException e) {
Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore");
} catch (InvocationTargetException e) {
Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore.");
}
//We don't save any form data in the application
settings.setSaveFormData(false);
settings.setSavePassword(false);
// Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist
// while we do this
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
Level16Apis.enableUniversalAccess(settings);
// Enable database
// We keep this disabled because we use or shim to get around DOM_EXCEPTION_ERROR_16
String databasePath = this.cordova.getActivity().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
settings.setDatabaseEnabled(true);
settings.setDatabasePath(databasePath);
settings.setGeolocationDatabasePath(databasePath);
// Enable DOM storage
settings.setDomStorageEnabled(true);
// Enable built-in geolocation
settings.setGeolocationEnabled(true);
// Enable AppCache
// Fix for CB-2282
settings.setAppCacheMaxSize(5 * 1048576);
String pathToCache = this.cordova.getActivity().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
settings.setAppCachePath(pathToCache);
settings.setAppCacheEnabled(true);
// Fix for CB-1405
// Google issue 4641
this.updateUserAgentString();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
if (this.receiver == null) {
this.receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateUserAgentString();
}
};
this.cordova.getActivity().registerReceiver(this.receiver, intentFilter);
}
// end CB-1405
pluginManager = new PluginManager(this, this.cordova);
jsMessageQueue = new NativeToJsMessageQueue(this, cordova);
exposedJsApi = new ExposedJsApi(pluginManager, jsMessageQueue);
resourceApi = new CordovaResourceApi(this.getContext(), pluginManager);
exposeJsInterface();
}
}
我们知道,PhoneGap拥有强大的访问设备的能力,包括照相机、传感器等等,就是通过JavaScript,所以在setup()函数中, setJavaScriptEnabled(true);setJavaScriptCanOpenWindowsAutomatically(true);这两个函数就必不可少了,当然,还需要其他一些相关的配置。
再来看看CordovaWebViewClient类,继承WebViewClient ,在onPageStarted()和onPageFinished()这两个函数,完成页面的加载。
public class CordovaWebViewClient extends WebViewClient {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// Flush stale messages.
this.appView.jsMessageQueue.reset();
// Broadcast message that page has loaded
this.appView.postMessage("onPageStarted", url);
// Notify all plugins of the navigation, so they can clean up if necessary.
if (this.appView.pluginManager != null) {
this.appView.pluginManager.onReset();
}
}
}
PluginManage类是插件管理类,我们在后面会提到。下面再来看看CordovaChromeClient类。
public class CordovaChromeClient extends WebChromeClient {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// Security check to make sure any requests are coming from the page initially
// loaded in webview and not another loaded in an iframe.
boolean reqOk = false;
if (url.startsWith("file://") || Config.isUrlWhiteListed(url)) {
reqOk = true;
}
// Calling PluginManager.exec() to call a native service using
// prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true]));
if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) {
JSONArray array;
try {
array = new JSONArray(defaultValue.substring(4));
String service = array.getString(0);
String action = array.getString(1);
String callbackId = array.getString(2);
String r = this.appView.exposedJsApi.exec(service, action, callbackId, message);
result.confirm(r == null ? "" : r);
} catch (JSONException e) {
e.printStackTrace();
return false;
}
}
// Sets the native->JS bridge mode.
else if (reqOk && defaultValue != null && defaultValue.equals("gap_bridge_mode:")) {
this.appView.exposedJsApi.setNativeToJsBridgeMode(Integer.parseInt(message));
result.confirm("");
}
// Polling for JavaScript messages
else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) {
String r = this.appView.exposedJsApi.retrieveJsMessages("1".equals(message));
result.confirm(r == null ? "" : r);
}
// Do NO-OP so older code doesn't display dialog
else if (defaultValue != null && defaultValue.equals("gap_init:")) {
result.confirm("OK");
}
// Show dialog
else {
final JsPromptResult res = result;
AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity());
dlg.setMessage(message);
final EditText input = new EditText(this.cordova.getActivity());
if (defaultValue != null) {
input.setText(defaultValue);
}
dlg.setView(input);
dlg.setCancelable(false);
dlg.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
String usertext = input.getText().toString();
res.confirm(usertext);
}
});
dlg.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
res.cancel();
}
});
dlg.create();
dlg.show();
}
return true;
}
}
CordovaChromeClient继承了WebChromeClient类,主要用于辅助WebView处理JavaScript的进度条、对话框等内容,所以实际上就是为了处理JS脚本。然而,PhoneGap在处理JS与Java方法交互的时候,并没有选择使用JsInterface,而是拦截prompt()方法进行JS脚本处理。在prompt()方法的参数有一个message,主要用于存放插件的应用信息,例如Camara插件的图片质量、是否可编辑、返回的图片类型等等,defaultValue存放插件信息,包括service(如Camera)、action(如getPicture)、callbackId、async等等。当prompt()方法拦截到这些信息的之后,执行了this.appView.exposedJsApi.exec(service, action, callbackId, message); 点进去可以发现,最后实际上是执行pluginManager.exec(service, action, callbackId, arguments); 那么,我们就不得不提一下PluginManage类了~~~
public class PluginManager {
/**
* Load plugins from res/xml/config.xml
*/
public void loadPlugins() {
//int id = this.ctx.getActivity().getResources().getIdentifier("config", "xml", this.ctx.getActivity().getClass().getPackage().getName());
int id = org.apache.cordova.R.xml.config;
Log.e(TAG, this.ctx.getActivity().getClass().getPackage().getName());
if (id == 0) {
this.pluginConfigurationMissing();
//We have the error, we need to exit without crashing!
return;
}
XmlResourceParser xml = this.ctx.getActivity().getResources().getXml(id);
int eventType = -1;
String service = "", pluginClass = "", paramType = "";
boolean onload = false;
boolean insideFeature = false;
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");
Log.d(TAG, "<plugin> tags are deprecated, please use <features> instead. <plugin> will no longer work as of Cordova 3.0");
onload = "true".equals(xml.getAttributeValue(null, "onload"));
}
//What is this?
else if (strNode.equals("url-filter")) {
this.urlMap.put(xml.getAttributeValue(null, "value"), service);
}
else if (strNode.equals("feature")) {
//Check for supported feature sets aka. plugins (Accelerometer, Geolocation, etc)
//Set the bit for reading params
insideFeature = true;
service = xml.getAttributeValue(null, "name");
}
else if (insideFeature && strNode.equals("param")) {
paramType = xml.getAttributeValue(null, "name");
if (paramType.equals("service")) // check if it is using the older service param
service = xml.getAttributeValue(null, "value");
else if (paramType.equals("package") || paramType.equals("android-package"))
pluginClass = xml.getAttributeValue(null,"value");
else if (paramType.equals("onload"))
onload = "true".equals(xml.getAttributeValue(null, "value"));
}
}
else if (eventType == XmlResourceParser.END_TAG)
{
String strNode = xml.getName();
if (strNode.equals("feature") || strNode.equals("plugin"))
{
PluginEntry entry = new PluginEntry(service, pluginClass, onload);
this.addService(entry);
//Empty the strings to prevent plugin loading bugs
service = "";
pluginClass = "";
insideFeature = false;
}
}
try {
eventType = xml.next();
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}</plugin></features></plugin>
loadPlugins()就是加载插件,配置信息从res/xml/config.xml文件中读取,重点来了,exec()方法。
public void exec(final String service, final String action, final String callbackId, final String rawArgs) {
if (numPendingUiExecs.get() > 0) { //判断当前等待UI线程执行的插件个数 大于0就往主线程消息队列里面发送一个消息。
numPendingUiExecs.getAndIncrement(); //统计当前主线程消息队列插件的个数并增加一个
this.ctx.getActivity().runOnUiThread(new Runnable() {
public void run() {
execHelper(service, action, callbackId, rawArgs);
numPendingUiExecs.getAndDecrement(); //执行完毕,统计当前主线程消息队列插件的个数并减少一个
}
});
} else { //如果主线程队列里面没有消息,直接调用execHelper
execHelper(service, action, callbackId, rawArgs);
}
}
private void execHelper(final String service, final String action, final String callbackId, final String rawArgs) {
CordovaPlugin plugin = getPlugin(service); //通过js传过来的名字找到对应的插件
if (plugin == null) {
Log.d(TAG, "exec() call to unknown plugin: " + service);
PluginResult cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
app.sendPluginResult(cr, callbackId);
return;
}
try {
// 我们知道,ExposedJSApi把接口暴露给js从jsmessagequeue队列里面取消息,谁往里面添加消息呢,没错,就是CallbackContext
CallbackContext callbackContext = new CallbackContext(callbackId, app);
// 终于来到我们熟悉的地方了,execute()函数,不就是我们的Activity继承CordovaActivity重写其execute()方法
boolean wasValidAction = plugin.execute(action, rawArgs, callbackContext);
if (!wasValidAction) {
PluginResult cr = new PluginResult(PluginResult.Status.INVALID_ACTION);
app.sendPluginResult(cr, callbackId);
}
} catch (JSONException e) {
PluginResult cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
app.sendPluginResult(cr, callbackId);
}
}
那么,exec() 执行成功的话,callbackContext就调用了success(),否则调用error(),这个在我们的定义的插件中就可以体现~~
public class MyPlugin extends CordovaPlugin {
private String helloAction = "helloAction";
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
Log.i("test", action);
if (action.equals(helloAction)) {
callbackContext.success("congratulation,success");
return true;
} else {
callbackContext.error("sorry,error");
return false;
}
}
}
那么,execute执行完毕时,就调用sendPluginResult()函数来把结果添加到消息队列
public void sendPluginResult(PluginResult result, String callbackId) {
this.jsMessageQueue.addPluginResult(result, callbackId); // webView调用的
}
而jsMessageQueue是什么样的呢,我们也来一起看看:
NativeToJsMessageQueue jsMessageQueue = new NativeToJsMessageQueue(this, cordova);// (CordovaInterface)cordova
exposedJsApi = new ExposedJsApi(pluginManager, jsMessageQueue);
我们前面说exposedJsApi包含了jsMessageQueue消息队列,同时把消息队列暴露给JS,原来就是这样子呀~~
so,在我们的cordova.js或者cordova.android.js中,你会发现retrieveJsMessages()进行了消息的相关处理
function pollOnce() {
var msg = nativeApiProvider.get().retrieveJsMessages();
androidExec.processMessages(msg);
}
define("cordova/plugin/android/promptbasednativeapi", function(require, exports, module) {
/**
* Implements the API of ExposedJsApi.java, but uses prompt() to communicate.
* This is used only on the 2.3 simulator, where addJavascriptInterface() is broken.
*/
/**
* 大概意思是由于Android2.3模拟器不支持addJavascriptInterface(),所以借助prompt()来和Native进行交互,
* Native端会在CordovaChromeClient.onJsPrompt()中拦截处理
*/
module.exports = {
exec: function(service, action, callbackId, argsJson) {// 调用Native API
return prompt(argsJson, 'gap:'+JSON.stringify([service, action, callbackId]));
},
setNativeToJsBridgeMode: function(value) {// 设置Native->JS的桥接模式
prompt(value, 'gap_bridge_mode:');
},
retrieveJsMessages: function() {// 接收消息
return prompt('', 'gap_poll:');
}
};
});
简单来说,PhoneGap框架流程就以下三步:
1、js 通过prompt接口往anroid native 发送消息
2、android 本地拦截WebChromeClient 对象的 onJsPrompt函数,截获消息
3、android本地截获到消息以后,通过Pluginmanager 把消息分发到目的插件,同时通过jsMessageQueue收集需要返回给js的数据
引用网上的一张图,一起来看看PhoneGap底层框架类图~~