Cordova—Android源码分析二:JS调用Native
在CordovaWebView初始化时,会根据Android版本的不同,初始化不同的JS调用Native的方法,当Android版本小于4.2(API 17)时,会采用prompt的方式处理JS的调用,当Android版本大于4.2时,会采用JavaScriptInterface的方式调用,初始化的方法在CordovaWebViewImpl的init(CordovaInterface cordova, List pluginEntries, CordovaPreferences preferences)方法中的:
engine.init(this, cordova, engineClient, resourceApi, pluginManager, nativeToJsMessageQueue);
engine的类型为CordovaWebViewEngine接口,它的实现类为SystemWebViewEngine,在init()方法的最后一行调用了exposeJsInterface()方法:
exposeJsInterface(webView, bridge);
在exposeJsInterface()方法中,根据Android版本的不同,初始化了不同的JS调用Android的方式:
private static void exposeJsInterface(WebView webView, CordovaBridge bridge) {
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
// Bug being that Java Strings do not get converted to JS strings automatically.
// This isn't hard to work-around on the JS side, but it's easier to just
// use the prompt bridge instead.
return;
}
SystemExposedJsApi exposedJsApi = new SystemExposedJsApi(bridge);
webView.addJavascriptInterface(exposedJsApi, "_cordovaNative");
}
在JS调用Native时,会根据_cordovaNative对象是否存在来判断使用哪种方式调用Native,SystemExposedJsApi类实现了ExposedJsApi,有以下三个方法:
public interface ExposedJsApi {
public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException;
public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException;
public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException;
}
SystemExposedJsApi如下:
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);
}
}
exec()方法是JS调用Native的方法。当Android版本低于4.2时,会采用prompt的方式,此方式在继承WebChromeClient的SystemWebChromeClient类中的onJsPrompt()方法中处理JS。
下面来分析Android 4.2以上版本的情况,当JS调用Native的exec方法时,会调用SystemExposedJsApi对象(“_cordovaNative”)的exec方法,从而调用CordovaBridge的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)方法,如下:
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());
}
}
在PluginManager的exec方法中,首先根据CordovaPlugin的名称得到插件,如果找不到插件,则设置错误回调信息并return。如果找到该插件,则调用:
boolean wasValidAction = plugin.execute(action, rawArgs, callbackContext);
并监听返回值,返回值标识是否正确执行,如果返回false,则设置错误信息并回调:
if (!wasValidAction) {
PluginResult cr = new PluginResult(PluginResult.Status.INVALID_ACTION);
callbackContext.sendPluginResult(cr);
}
同时,还会监控执行时间,如果时间过长,则警告用户插件执行阻塞了主线程,为了防止阻塞主线程,需要在子线程中进行耗时操作,Cordova提供了一个线程池供插件调用,在CordovaPlugin中使用以下方法获取线程时:
cordova.getThreadPool();
以上为Cordova中JS调用本地插件的全部过程。