我们先通过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;
}
}
看到这里对于我们本次的需求,在外壳添加几个插件就可以动手了。