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底层框架类图~~