Android工程中调用Cordova插件(相册)源码详情解析

本文详细介绍了Android工程中如何调用Cordova相册插件的源码流程。从h5调用getPicture()方法开始,深入到Cordova的exec()方法,探讨了js与原生代码的交互方式,包括SystemWebViewEngine和SystemExposedJsApi的使用,以及PluginManager和CordovaPlugin在执行过程中的角色。最后,分析了消息队列和prompt方法在通信中的作用,为理解和自定义Cordova插件提供了清晰的指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们先通过loadUrl(url)加载我们的本地h5项目,使用loadUrl这个方法一定是继承自CordovaActivity,cordovaActivity的onCreate方法中调用了loadConfig(),这个方法主要是加载配置文件(res/xml/config.xml )中的信息。

h5中如何调用引入的cordova插件呢,以相册选取为例,下面我们来看下:

navigator.camera.getPicture(cameraSuccess, cameraError, cameraOptions);

我们通过上面代码调用Camera的getPicture()方法,看下Camera.js的代码:

cameraExport.getPicture = function (successCallback, errorCallback, options) {
    argscheck.checkArgs('fFO', 'Camera.getPicture', arguments);
    options = options || {};
    var getValue = argscheck.getValue;

    var quality = getValue(options.quality, 50);
    var destinationType = getValue(options.destinationType, Camera.DestinationType.FILE_URI);
    var sourceType = getValue(options.sourceType, Camera.PictureSourceType.CAMERA);
    var targetWidth = getValue(options.targetWidth, -1);
    var targetHeight = getValue(options.targetHeight, -1);
    var encodingType = getValue(options.encodingType, Camera.EncodingType.JPEG);
    var mediaType = getValue(options.mediaType, Camera.MediaType.PICTURE);
    var allowEdit = !!options.allowEdit;
    var correctOrientation = !!options.correctOrientation;
    var saveToPhotoAlbum = !!options.saveToPhotoAlbum;
    var popoverOptions = getValue(options.popoverOptions, null);
    var cameraDirection = getValue(options.cameraDirection, Camera.Direction.BACK);

    var args = [quality, destinationType, sourceType, targetWidth, targetHeight, encodingType,
        mediaType, allowEdit, correctOrientation, saveToPhotoAlbum, popoverOptions, cameraDirection];

    exec(successCallback, errorCallback, 'Camera', 'takePicture', args);
    // XXX: commented out
    // return new CameraPopoverHandle();
};

在Camera.js中调用了exec(successCallback, errorCallback, 'Camera', 'takePicture', args),exec在Camera.js中定义了,如下

var exec = require('cordova/exec');

在cordova.js中定义了这个方法如下:

// file: /Users/steveng/repo/cordova/cordova-android/cordova-js-src/exec.js

define("cordova/exec", function(require, exports, module) {

nativeApiProvider = require('cordova/android/nativeapiprovider'),

utils = require('cordova/utils'),

base64 = require('cordova/base64'),

channel = require('cordova/channel'),

// JS->Native的可选交互形式

jsToNativeModes = {

PROMPT: 0, // 基于prompt()的交互

JS_OBJECT: 1// 基于JavascriptInterface的交互   

},

// Native->JS的可选交互形式

nativeToJsModes = {

POLLING: 0,// 轮询(JS->Native自助获取消息)

// 使用 webView.loadUrl("javascript:") 来执行消息,解决软键盘的Bug  

LOAD_URL: 1,

// 拦截事件监听,使用online/offline事件来告诉JS获取消息  

// 默认值 NativeToJsMessageQueue.DEFAULT_BRIDGE_MODE=2  

ONLINE_EVENT: 2,

EVAL_BRIDGE: 3

},

jsToNativeBridgeMode, // Set lazily.

nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE,

pollEnabled = false,

bridgeSecret = -1;



var messagesFromNative = [];

var isProcessing = false;

var resolvedPromise = typeof Promise == 'undefined' ? null : Promise.resolve();

var nextTick = resolvedPromise ? function(fn) { resolvedPromise.then(fn); } : function(fn) { setTimeout(fn); };

function androidExec(success, fail, service, action, args) {

if (bridgeSecret < 0) {

// If we ever catch this firing, we'll need to queue up exec()s

// and fire them once we get a secret. For now, I don't think

// it's possible for exec() to be called since plugins are parsed but

// not run until until after onNativeReady.

throw new Error('exec() called without bridgeSecret');

}

// 默认采用JavascriptInterface交互方式  

if (jsToNativeBridgeMode === undefined) {

androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);

}



// If args is not provided, default to an empty array

args = args || [];



// Process any ArrayBuffers in the args into a string.

for (var i = 0; i < args.length; i++) {

if (utils.typeName(args[i]) == 'ArrayBuffer') {

args[i] = base64.fromArrayBuffer(args[i]);

}

}



var callbackId = service + cordova.callbackId++,

argsJson = JSON.stringify(args);

if (success || fail) {

cordova.callbacks[callbackId] = {success:success, fail:fail};

}

// 选择合适的交互方式和Native进行交互  

// 根据Native端NativeToJsMessageQueue.DISABLE_EXEC_CHAINING的配置,回传消息可以是同步或者异步  

// 默认是同步的,返回PluginResult对象的JSON串。异步的话messages为空。  

var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);

 // 如果参数被传递到Java端,但是接收到的是null,切换交互方式到prompt()在执行一次,Galaxy S2在传递某些Unicode字符的

//时候少数情况下有问题

if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") {

androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);

androidExec(success, fail, service, action, args);

// 执行完成后,把交互方式再切回JavascriptInterface

androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);

} else if (msgs) {

messagesFromNative.push(msgs);

// 处理Native返回的消息 

nextTick(processMessages);

}

}

......

module.exports = androidExec;

});

主要代码:var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);

nativeApiProvider.get()最终返回nativeApi ,看下nativeApiProvider中是如何定义的

// file: /Users/steveng/repo/cordova/cordova-android/cordova-js-src/android/nativeapiprovider.js

define("cordova/android/nativeapiprovider", function(require, exports, module) {

// WebView中是否通过addJavascriptInterface提供了访问ExposedJsApi.java的_cordovaNative对象 如果不存在选择prompt()形

//式的交互方式  

var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativeapi');

var currentApi = nativeApi;

module.exports = {

get: function() { return currentApi; },

setPreferPrompt: function(value) {

currentApi = value ? require('cordova/android/promptbasednativeapi') : nativeApi;

},

// Used only by tests.

set: function(value) {

currentApi = value;

}

};

});

var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativeapi');

如果this._cordovaNative有值

这个代码是如何和java代码通信的呢,看下SystemWebViewEngine.java:

public class SystemWebViewEngine implements CordovaWebViewEngine {

......

// Yeah, we know. It'd be great if lint was just a little smarter.

@SuppressLint("AddJavascriptInterface")

private static void exposeJsInterface(WebView webView, CordovaBridge bridge) {

SystemExposedJsApi exposedJsApi = new SystemExposedJsApi(bridge);

webView.addJavascriptInterface(exposedJsApi, "_cordovaNative");

}

}

我们看到了标识_cordovaNative这个字符,在看下调用的SystemExposedJsApi.java中的exec()方法:

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);
    }
}

加了注解@JavascriptInterface,将方法暴露给js,js就可以直接调用这个方法了和原生webview中的java与js交互完全一样,也就是说,nativeApiProvider.get().exec这个方法最终调用了SystemExposedJsApi中的exec方法,然后调用CordovaBridge中的jsExec方法(bridge.jsExec),若this._cordovaNative没值则调用promptbasednativeapi.js中的exec方法,

// file: /Users/steveng/repo/cordova/cordova-android/cordova-js-src/android/promptbasednativeapi.js

define("cordova/android/promptbasednativeapi", function(require, exports, module) {

// prompt()来和Native进行交互,Native端会在SystemWebChromeClient.onJsPrompt()中拦截处理  

module.exports = {

exec: function(bridgeSecret, service, action, callbackId, argsJson) {

return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));

},

setNativeToJsBridgeMode: function(bridgeSecret, value) {

prompt(value, 'gap_bridge_mode:' + bridgeSecret);

},

//接受消息

retrieveJsMessages: function(bridgeSecret, fromOnlineEvent) {

return prompt(+fromOnlineEvent, 'gap_poll:' + bridgeSecret);

}

};



});

 js 通过prompt弹窗往anroid native 发送消息

prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]))

在java端是如何截取这个消息呢,SystemWebChromeClient这个类继承了WebChromeClient,并在onJsPrompt方法中截取了这个弹窗消息:

public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
    // Unlike the @JavascriptInterface bridge, this method is always called on the UI thread.
    String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);
    if (handledRet != null) {
        result.confirm(handledRet);
    } else {
        dialogsHelper.showPrompt(message, defaultValue, new CordovaDialogsHelper.Result() {
            @Override
            public void gotResult(boolean success, String value) {
                if (success) {
                    result.confirm(value);
                } else {
                    result.cancel();
                }
            }
        });
    }
    return true;
}

调用了CordovaBridge.java中的promptOnJsPrompt方法:

public String promptOnJsPrompt(String origin, String message, String defaultValue) {
    if (defaultValue != null && defaultValue.length() > 3 && defaultValue.startsWith("gap:")) {
        JSONArray array;
        try {
            array = new JSONArray(defaultValue.substring(4));
            int bridgeSecret = array.getInt(0);
            String service = array.getString(1);
            String action = array.getString(2);
            String callbackId = array.getString(3);
            String r = jsExec(bridgeSecret, service, action, callbackId, message);
            return r == null ? "" : r;
        } catch (JSONException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return "";
    }
    // Sets the native->JS bridge mode.
    else if (defaultValue != null && defaultValue.startsWith("gap_bridge_mode:")) {
        try {
            int bridgeSecret = Integer.parseInt(defaultValue.substring(16));
            jsSetNativeToJsBridgeMode(bridgeSecret, Integer.parseInt(message));
        } catch (NumberFormatException e){
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return "";
    }
    // Polling for JavaScript messages
    else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) {
        int bridgeSecret = Integer.parseInt(defaultValue.substring(9));
        try {
            String r = jsRetrieveJsMessages(bridgeSecret, "1".equals(message));
            return r == null ? "" : r;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return "";
    }
    else if (defaultValue != null && defaultValue.startsWith("gap_init:")) {
        // Protect against random iframes being able to talk through the bridge.
        // Trust only pages which the app would have been allowed to navigate to anyway.
        if (pluginManager.shouldAllowBridgeAccess(origin)) {
            // Enable the bridge
            int bridgeMode = Integer.parseInt(defaultValue.substring(9));
            jsMessageQueue.setBridgeMode(bridgeMode);
            // Tell JS the bridge secret.
            int secret = generateBridgeSecret();
            return ""+secret;
        } else {
            LOG.e(LOG_TAG, "gap_init called from restricted origin: " + origin);
        }
        return "";
    }
    return null;
}

var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativeapi');

var currentApi = nativeApi;

最后也是将数据传递到了jsExec方法中,即nativeApi不管是哪种js与native的交互都是回调了CordovaBridge.java中的jsExec方法,在看下jsExec方法:

public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
    if (!verifySecret("exec()", bridgeSecret)) {
        return null;
    }
    // If the arguments weren't received, send a message back to JS.  It will switch bridge modes and try again.  See CB-2666.
    // We send a message meant specifically for this case.  It starts with "@" so no other message can be encoded into the same string.
    if (arguments == null) {
        return "@Null arguments.";
    }

    jsMessageQueue.setPaused(true);
    try {
        // Tell the resourceApi what thread the JS is running on.
        CordovaResourceApi.jsThread = Thread.currentThread();

        pluginManager.exec(service, action, callbackId, arguments);
        String ret = null;
        if (!NativeToJsMessageQueue.DISABLE_EXEC_CHAINING) {
            ret = jsMessageQueue.popAndEncode(false);
        }
        return ret;
    } catch (Throwable e) {
        e.printStackTrace();
        return "";
    } finally {
        jsMessageQueue.setPaused(false);
    }
}

里面调用了pluginManager.exec(service, action, callbackId, arguments)方法,在PluginManager.java中

public void exec(final String service, final String action, final String callbackId, final String rawArgs) {
    CordovaPlugin plugin = getPlugin(service);
    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;
    }
    CallbackContext callbackContext = new CallbackContext(callbackId, app);
    try {
        long pluginStartTime = System.currentTimeMillis();
        boolean wasValidAction = plugin.execute(action, rawArgs, callbackContext);
        long duration = System.currentTimeMillis() - pluginStartTime;

        if (duration > SLOW_EXEC_WARNING_THRESHOLD) {
            LOG.w(TAG, "THREAD WARNING: exec() call to " + service + "." + action + " blocked the main thread for " + duration + "ms. Plugin should use CordovaInterface.getThreadPool().");
        }
        if (!wasValidAction) {
            PluginResult cr = new PluginResult(PluginResult.Status.INVALID_ACTION);
            callbackContext.sendPluginResult(cr);
        }
    } catch (JSONException e) {
        PluginResult cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
        callbackContext.sendPluginResult(cr);
    } catch (Exception e) {
        LOG.e(TAG, "Uncaught exception from plugin", e);
        callbackContext.error(e.getMessage());
    }
}
CordovaPlugin plugin = getPlugin(service);根据service名称获取插件对象
 /**
     * Get the plugin object that implements the service.
     * If the plugin object does not already exist, then create it.
     * If the service doesn't exist, then return null.
     *
     * @param service       The name of the service.
     * @return              CordovaPlugin or null
     */
    public CordovaPlugin getPlugin(String service) {
        CordovaPlugin ret = pluginMap.get(service);
        if (ret == null) {
            PluginEntry pe = entryMap.get(service);
            if (pe == null) {
                return null;
            }
            if (pe.plugin != null) {
                ret = pe.plugin;
            } else {
                ret = instantiatePlugin(pe.pluginClass);
            }
            ret.privateInitialize(service, ctx, app, app.getPreferences());
            pluginMap.put(service, ret);
        }
        return ret;
    }

然后调用了plugin.execute(action, rawArgs, callbackContext)方法,execute是CordovaPlugin.java中的一个方法,而我们的拍照类CameraLauncher 继承了CordovaPlugin,并重写了execute方法,在CordovaPlugin中有这么一行注释说的很清楚:

/**
 * Plugins must extend this class and override one of the execute methods.
 */
public class CordovaPlugin {......}

CameraLauncher.java代码如下:

public class CameraLauncher extends CordovaPlugin implements MediaScannerConnectionClient {

......

public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {

......

if (action.equals("takePicture")) {

......

return true;

}

return false;

}

}

最初代码是通过

exec(successCallback, errorCallback, 'Camera', 'takePicture', args);来调用相册的,字符串takePicture就是通过这一步步传递过来的,当然调用完之后还有一系列的拿到返回值的操作,在CameraLauncher中的getImage方法中,通过startActivityForResult方法及onActivityResult方法获取选中的图片信息,在processResultFromGallery方法中调用CallbackContext.java中的success方法,然后通过webView.sendPluginResult(pluginResult, callbackId)方法调用了webview的sendPluginresult方法:
public void sendPluginResult(PluginResult cr, String callbackId) {
        nativeToJsMessageQueue.addPluginResult(cr, callbackId);
    }
private final LinkedList<JsMessage> queue = new LinkedList<JsMessage>();
queue.add(message);

将信息添加到消息队列中,js通过prompt(+fromOnlineEvent, 'gap_poll:' + bridgeSecret)接受消息,字符串gap_poll后面会用到

前面说过在CordovaBridge中的promptOnJsPrompt方法中会拦截prompt方法,如下:

  // Polling for JavaScript messages
        else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) {
            int bridgeSecret = Integer.parseInt(defaultValue.substring(9));
            try {
                String r = jsRetrieveJsMessages(bridgeSecret, "1".equals(message));
                return r == null ? "" : r;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return "";
        }

 最终调用了NativeToJsMessageQueue.java中的popAndEncode方法返回队列中的消息数据,如下:

   /**
     * Combines and returns queued messages combined into a single string.
     * Combines as many messages as possible, while staying under MAX_PAYLOAD_SIZE.
     * Returns null if the queue is empty.
     */
    public String popAndEncode(boolean fromOnlineEvent) {
        synchronized (this) {
            if (activeBridgeMode == null) {
                return null;
            }
            activeBridgeMode.notifyOfFlush(this, fromOnlineEvent);
            if (queue.isEmpty()) {
                return null;
            }
            int totalPayloadLen = 0;
            int numMessagesToSend = 0;
            for (JsMessage message : queue) {
                int messageSize = calculatePackedMessageLength(message);
                if (numMessagesToSend > 0 && totalPayloadLen + messageSize > MAX_PAYLOAD_SIZE && MAX_PAYLOAD_SIZE > 0) {
                    break;
                }
                totalPayloadLen += messageSize;
                numMessagesToSend += 1;
            }

            StringBuilder sb = new StringBuilder(totalPayloadLen);
            for (int i = 0; i < numMessagesToSend; ++i) {
                JsMessage message = queue.removeFirst();
                packMessage(message, sb);
            }

            if (!queue.isEmpty()) {
                // Attach a char to indicate that there are more messages pending.
                sb.append('*');
            }
            String ret = sb.toString();
            return ret;
        }
    }

 

看到这里对于我们本次的需求,在外壳添加几个插件就可以动手了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

博主逸尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值