PhoneGap 底层框架实现原理 详解

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


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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值