PhoneGap 底层框架实现原理 详解

来源:互联网 发布:mac air桌面壁纸 高清 编辑:程序博客网 时间:2024/05/21 03:59

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";@Overridepublic 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)cordovaexposedJsApi = 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底层框架类图~~


至此,PhoneGap底层框架的流程就差不多走完了,这边主要是为大家提供一个流程思路,以便减少大家在研究底层时所花的时间。具体细节上的东西,我也还是有很多不明白,大家有什么好的东西,也可以一起分享交流~~花了大半天时间终于写完~睡个觉先~~

8 0