前记:这里将19年中的Android Cordova主要源码走读分享出来,后期目标基于原有的cordova 框架扣除多余的代码,包括 js/java文件等 同时扩展容器的可选择性及 结合aop功能简化native 注册机制,合并同ios的使用注册文件模版为单一注入引擎文件,原有的native 到js的bridge使用方式不变,以此作为对之前技术输出的总结
1.Cordova SDK整体结构
cordova_whole.png2.Cordova native端加载流程图
cordova_load.pngnote: native插件的注册h5容器页面【不管是Activity 还是 Fragment 在初始化onCreate/onCreateView时执行注入native插件注入】
a. 解析并读取 corodva_plugins.xml中关于插件的信息生成PluginEntry集合
b. 在loadUrl【加载h5页面前】初始化好webview并根据PluginEntry中的onLoad值来确定是否提前初始化对应的插件
c.loadUrl执行,加载h5启动页到初始化好的webview中
d.其他onLoad 为false的大多数插件都是在具体js执行对应native插件能力时通过反射完成初始化并最终调用native 插件中的原生能力
2.1 注入native端xml文件中声明的 plugin【xml提供了后续扩展解耦plugin添加的入口】,解析xml并用全局map维护,指定onload为true的先行初始化,其他native端plugin 懒加载
// CordovaActivity 的onCreate
//这里是corodva native插件的懒加载注入入口
loadConfig(pluginEntity);
// 加载xml中的native plugin入口
@SuppressWarnings("deprecation")
protected void loadConfig(ArrayList<PluginEntry> list) {
ConfigXmlParser parser = new ConfigXmlParser();
if (!CollectionUtils.isEmpty(list)) {
parser.setDynamicEntity(list);
}
parser.parse(this);
preferences = parser.getPreferences();
preferences.setPreferencesBundle(getIntent().getExtras());
launchUrl = parser.getLaunchUrl();
//xml 解析得到native xml中声明的 plugin 包装对象map 并在下面loadUrl的 init方法中传递到PluginManager中
pluginEntries = parser.getPluginEntries();
Config.parser = parser;
}
2.2 init() : 初始化WebView 对应代理对象重载,设置webview的属性(settings),暴露native 对象给js作为后续交互的bridge
public void loadUrl(String url) {
if (appView == null) {
init();
}
// If keepRunning
this.keepRunning = preferences.getBoolean("KeepRunning", true);
appView.loadUrlIntoView(url, true);
}
// com.xxx.xxx.core.CordovaActivity#init 方法提供了子类重写init方法
protected void init() {
appView = makeWebView();
createViews();
if (!appView.isInitialized()) {
appView.init(cordovaInterface, pluginEntries, preferences);
}
cordovaInterface.onCordovaInit(appView.getPluginManager());
// Wire the hardware volume controls to control media if desired.
String volumePref = preferences.getString("DefaultVolumeStream", "");
if ("media".equals(volumePref.toLowerCase(Locale.ENGLISH))) {
setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
}
// ContainerActivity#makeWebView
@Override
protected CordovaWebView makeWebView() {
mSystemWebView = new NormalWebView(this);
mEngine = new SystemWebViewEngine((NormalWebView)mSystemWebView);
mFlWeb.addView((NormalWebView)mSystemWebView);
mCordovaWebView = new CordovaWebViewImpl(mEngine);
mSystemWebView.resetUserAgent( " " + userAgent);
//这里抽象了 IWebview 【便于替换X5Webview等其他扩展WebView的实现】
mSystemWebView.resetWebViewClient(this, mEngine);
//同理在NormalWebview中注入了重写的WebViewClient和 WebChromeClient
mSystemWebView.resetWebChromeClient(this, mEngine);
return mCordovaWebView;
}
2.3 证书校验说明
//证书校验调用入口
com.xxx.RedirectSystemWebViewClient#onReceivedSslError
@Override
public void onReceivedSslError(WebView webView, SslErrorHandler handler, SslError sslError) {
// super.onReceivedSslError(webView, handler, sslError); 注释父类实现
//入口封装传入参数确定是否证书校验
if (isCheck()) {
//检验证书颁发机构以及证书的合法性 证书确实是发给指定域名的且为当前设备的根证书所信任
if (SSLChecker.checkSSLCNCertOCFT(sslError.getCertificate())) {
handler.proceed(); // 如果证书一致,忽略错误
} else {
LogUtil.w("onReceivedSslError", "onReceivedSslError" + "cancel" + sslError);
handler.cancel();
}
// }
} else {
handler.proceed();
}
if (client != null) {
client.onReceivedSslError();
}
}
});
//init方法调用到engine 的 init方法, 初始化WebView相关设置,最后调用到 com.xxx.xxx.engine.SystemWebViewEngine#exposeJsInterface
//1.WebView相关设置
initWebViewSettings();
//2.暴露对象给js rename:“_cordovaNative”
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注入native端 SystemExposedJsApi的对象 ,并以 _cordovaNative为名在js端使用
webView.addJavascriptInterface(exposedJsApi, "_cordovaNative");
}
2.4 重载代理接口处理动态拦截部分[当前展示为非动态加载代码块]
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
Context context = view.getContext();
if(url.endsWith(".js") && url.contains("local_intercept")) {
int intercept = url.indexOf("local_intercept");
url = url.substring(intercept+"local_intercept/".length(),url.length());
return getWebResourceResponse(url, context);
}
return super.shouldInterceptRequest(view, url);
}
@SuppressLint("NewApi")
private WebResourceResponse getWebResourceResponse(String url, Context context){
WebResourceResponse res = null;
try {
InputStream instream = context.getResources().getAssets().open(
url);
res = new WebResourceResponse("text/javascript",
"UTF-8", instream);
} catch (IOException e) {
e.printStackTrace();
}
return res;
}
3.Cordova js部分流程图
cordova_js_init.pngnote:js插件的注册
js插件的注册从loadUrl即 入口index页面加载开始【这里以容器的测试页面为例子说明】 核心点是 : cordova.js 源码解读
3.1 声明 cordova 核心能力引用入口api package/import 和 define/require 执行Cordova.js文件加载
js从上至下解析执行,corodva.js 中的 define 先行,声明 cordova.js文件中内置的初始化插件
包括
cordova/
channel/
nativeapiprovider/
base64/
init/
modulemapper/
platform/
app/
pluginloader/
utils/
等其他自带基础组件
var require, //类java的package和import全局声明
define;
(function () {
var modules = {},
// Stack of moduleIds currently being built.
requireStack = [],
// Map of module ID -> index into requireStack of modules currently being built.
inProgressModules = {},
SEPARATOR = ".";
3 执行modules中的item module(在第1步中有声明定义)这里执行factory对应的 function(require, exports, module){...} 方法调用a/b 栈回调退出b/a 将module.exports赋给require点使用
require的作用:require后立即使用export出来的能力
require对应插件,执行插件中的代码
function build(module) {
var factory = module.factory,
//将require的能力传递到js插件中使用
localRequire = function (id) {
var resultantId = id;
//Its a relative path, so lop off the last portion and add the id (minus "./")
if (id.charAt(0) === ".") {
resultantId = module.id.slice(0, module.id.lastIndexOf(SEPARATOR)) + SEPARATOR + id.slice(2);
}
return require(resultantId);
};
module.exports = {};
//移除要exports的js插件对象绑定的 factory【因为已经exports 下次require时,直接对外输出,不再重复build流程避免循环 见:[X] 处】
delete module.factory;
factory(localRequire, module.exports, module); [b]
//绑定当前require的js 插件的factory到js插件所映射的module对象中【后续再绑定到window上供h5全局调用】
return module.exports;
}
2 此id为define func 的第一个参数 id 即 插件数组js文件中单个item的id
require = function (id) {
if (!modules[id]) {
//首先你得先define到modules数组中才能使用
throw "module " + id + " not found";
} else if (id in inProgressModules) {
//说明js插件存在循环require【应杜绝,死循环】
var cycle = requireStack.slice(inProgressModules[id]).join('->') + '->' + id;
throw "Cycle in require graph: " + cycle;
}
// require->build js插件后
if (modules[id].factory) {[X1]
try {
inProgressModules[id] = requireStack.length;
requireStack.push(id);
return build(modules[id]); [a]
} finally {
//移除维护的额外数组js插件元素,确保当前的js require的正常运行
delete inProgressModules[id];
requireStack.pop();
}
}
//执行完factory内exports的导出后,跳过[X1],直接返回前面已绑定的exports
return modules[id].exports; [X2]
};
1 声明初始化modules数组中的 插件对象【可对比具体的插件define得知属性id 和 factory id在后面中有判断是否有加载中判断 factory作为整个js export 能力的入口】
define = function (id, factory) {
if (modules[id]) {
throw "module " + id + " already defined";
}
modules[id] = {
id: id,
factory: factory
};
};
define.remove = function (id) {
delete modules[id];
};
define.moduleMap = modules;
})();
3.2 Cordova.js文件末尾调用:window.cordova = require('cordova'); 启动整个cordova js的环境初始化
a).初始化cordova加载环境,包括native和js的交互,事件监听注册以及在后续可能使用的api声明
b).将cordova主默认js插件添加到全局可用
c).加载外置插件plugin_list等js中的插件【动态加载js文件到 document,加载类cordova.js 中的各子js插件 包括 define 使用时的require 以及factory的exports 】
setTimeout(function() {
pluginloader.load(function() {//这个function即为最终回调调用的callback
channel.onPluginsReady.fire(); [X3]
});
}, 0);
//改造扩展加载外置js插件方法
exports.load = function(callback) {
var pathPrefix = findCordovaPath();
if (pathPrefix === null) {
console.log('Could not find cordova.js script tag. Plugin loading may fail.');
pathPrefix = '';
}
// injectIfNecessary('cordova/plugin_list', pathPrefix + 'cordova_plugins.js', function() {
// var moduleList = require("cordova/plugin_list");
// handlePluginsObject(pathPrefix, moduleList, callback);
// }, callback);
// console.log("log", new Date().getTime());
var cordovaTime;
var bridgeTime;
injectIfNecessary('cordova/plugin_list', pathPrefix + 'cordova_plugins.js', function() { [X:plugin_list]
cordovaTime = new Date().getTime();
// console.log("log",cordovaTime, bridgeTime);
if(bridgeTime > 0 && cordovaTime>=bridgeTime) {
var bridgeList = require("cordova/bridge_list");
var moduleList = require("cordova/plugin_list");
var pluginList = moduleList.concat(bridgeList);
// console.log("log", pluginList, new Date().getTime());
handlePluginsObject(pathPrefix, pluginList, callback);
}
// handlePluginsObject(pathPrefix, moduleList, callback);
}, callback);
// console.log("log", new Date().getTime());
injectIfNecessary('cordova/bridge_list', pathPrefix + 'bridge_plugins.js', function() {
[X:plugin_list]
bridgeTime = new Date().getTime();
// console.log("log", bridgeTime, cordovaTime);
if(cordovaTime>0 && bridgeTime >= cordovaTime) {
var bridgeList = require("cordova/bridge_list");
var moduleList = require("cordova/plugin_list");
var pluginList = moduleList.concat(bridgeList);
// console.log("log", pluginList, new Date().getTime());
handlePluginsObject(pathPrefix, pluginList, callback);
}
}, callback);
};
});
// file: src/common/pluginloader_b.js
define("cordova/pluginloader_b", function(require, exports, module) {
var modulemapper = require('cordova/modulemapper');
// Handler for the cordova_plugins.js content.
// See plugman's plugin_loader.js for the details of this object.
function handlePluginsObject(moduleList) {
// if moduleList is not defined or empty, we've nothing to do
if (!moduleList || !moduleList.length) {
return;
}
// Loop through all the modules and then through their clobbers and merges.
for (var i = 0, module; module = moduleList[i]; i++) {
if (module.clobbers && module.clobbers.length) {
for (var j = 0; j < module.clobbers.length; j++) {
modulemapper.clobbers(module.id, module.clobbers[j]);
}
}
if (module.merges && module.merges.length) {
for (var k = 0; k < module.merges.length; k++) {
modulemapper.merges(module.id, module.merges[k]);
}
}
// Finally, if runs is truthy we want to simply require() the module.
if (module.runs) {
modulemapper.runs(module.id);
}
}
}
d).外置js插件script脚本加载完成回调执行onScriptLoadingComplete,添加外置插件到modulemapper对象上,最后回调onPluginsReady事件 [x3]
function handlePluginsObject(path, moduleList, finishPluginLoading) {
// Now inject the scripts.
var scriptCounter = moduleList.length;
if (!scriptCounter) {
finishPluginLoading();
return;
}
function scriptLoadedCallback() {
//每动态注入 一个moduleList的元素 scriptCounter减1,最后执行onScriptLoadingComplete [X6]节点
if (!--scriptCounter) {
onScriptLoadingComplete(moduleList, finishPluginLoading);
}
}
for (var i = 0; i < moduleList.length; i++) {
//动态注入 moduleList 中 的js文件,完成后回调 scriptLoadedCallback方法
injectIfNecessary(moduleList[i].id, path + moduleList[i].file, scriptLoadedCallback);
}
}
// 动态注入js入口
function injectIfNecessary(id, url, onload, onerror) {
onerror = onerror || onload;
if (id in define.moduleMap) {
onload();
} else {
exports.injectScript(url, function() {
if (id in define.moduleMap) {
onload();
} else {
onerror();
}
}, onerror);
}
}
//动态注入js的实现 加载js完成后回调script的onload方法 最终调用 injectIfNecessary 的 scriptLoadedCallback
exports.injectScript = function(url, onload, onerror) {
var script = document.createElement("script");
// onload fires even when script fails loads with an error.
script.onload = onload; //scriptLoadedCallback 或 [X:plugin_list]
// onerror fires for malformed URLs.
script.onerror = onerror;
script.src = url;
// console.log('log', script.src);
document.head.appendChild(script);
};
// [X6] 脚本加载完成后 根据js对象声明的不同属性(clobbers/merges)添加到 modulemapper 对象中
function onScriptLoadingComplete(moduleList, finishPluginLoading) {
// Loop through all the plugins and then through their clobbers and merges.
for (var i = 0, module; module = moduleList[i]; i++) {
if (module.clobbers && module.clobbers.length) {
for (var j = 0; j < module.clobbers.length; j++) {
modulemapper.clobbers(module.id, module.clobbers[j]);
}
}
if (module.merges && module.merges.length) {
for (var k = 0; k < module.merges.length; k++) {
modulemapper.merges(module.id, module.merges[k]);
}
}
// Finally, if runs is truthy we want to simply require() the module.
if (module.runs) {
modulemapper.runs(module.id);
}
}
finishPluginLoading();
}
e).将js插件挂载到window上【plugin_list/bridge_list】后,前置注册的 deviceready 事件fire 最后回调 onDeviceReady方法 js端加载结束
//([x3]调用)channel.join
Channel.prototype.fire = function(e) {
var fail = false,
fireArgs = Array.prototype.slice.call(arguments);
// Apply stickiness.
if (this.state == 1) {
this.state = 2;
this.fireArgs = fireArgs;
}
if (this.numHandlers) {
// Copy the values first so that it is safe to modify it from within
// callbacks.
var toCall = [];
for (var item in this.handlers) {//handlers中存储了前置subscribe的func 即 之前注入到 channel 的 join方法[X4],这里push后 toCall同步 handlers内元素
toCall.push(this.handlers[item]);
}
for (var i = 0; i < toCall.length; ++i) {
//执行channel中的join方法【可延伸:js中 apply 和 call的区别】
toCall[i].apply(this, fireArgs);
}
if (this.state == 2 && this.numHandlers) {
this.numHandlers = 0;
this.handlers = {};
this.onHasSubscribersChange && this.onHasSubscribersChange();
}
}
};
[X5]
channel.join(function() {
//挂载外置js插件到window上
modulemapper.mapModules(window);
platform.initialize && platform.initialize();
// Fire event to notify that all objects are created
channel.onCordovaReady.fire();
// Fire onDeviceReady event once page has fully loaded, all
// constructors have run and cordova info has been received from native
// side.
channel.join(function() {
require('cordova').fireDocumentEvent('deviceready');
}, channel.deviceReadyChannelsArray);
}, platformInitChannelsArray);
channel = {
/**
* Calls the provided function only after all of the channels specified
* have been fired. All channels must be sticky channels.
*/
join: function(h, c) {
var len = c.length,
i = len,
f = function() {
//数组中的事件fire后回调用,最后一个元素即onPluginReady fire时 调用 [X5]注入的func->h()
if (!(--i)) h();
};
for (var j=0; j<len; j++) {
if (c[j].state === 0) {
throw Error('Can only use join with sticky channels.');
}
//事件订阅,fire时触发f->func【这里为channel.join调用时的func】
c[j].subscribe(f);
}
if (!len) h();
},
create: function(type) {
return channel[type] = new Channel(type, false);
},
createSticky: function(type) {
return channel[type] = new Channel(type, true);
},
/**
* cordova Channels that must fire before "deviceready" is fired.
*/
deviceReadyChannelsArray: [],
deviceReadyChannelsMap: {},
/**
* Indicate that a feature needs to be initialized before it is ready to be used.
* This holds up Cordova's "deviceready" event until the feature has been initialized
* and Cordova.initComplete(feature) is called.
*
* @param feature {String} The unique feature name
*/
waitForInitialization: function(feature) {
if (feature) {
var c = channel[feature] || this.createSticky(feature);
this.deviceReadyChannelsMap[feature] = c;
this.deviceReadyChannelsArray.push(c);
}
},
/**
* Indicate that initialization code has completed and the feature is ready to be used.
*
* @param feature {String} The unique feature name
*/
initializationComplete: function(feature) {
var c = this.deviceReadyChannelsMap[feature];
if (c) {
c.fire();
}
}
};
//这里的obj 为传递过来的window 即完成了最终exports出来的js能力挂载到window上
function clobber(obj, key, value) {
exports.replaceHookForTesting(obj, key);
var needsProperty = false;
try {
obj[key] = value;
} catch (e) {
needsProperty = true;
}
// Getters can only be overridden by getters.
if (needsProperty || obj[key] !== value) {
utils.defineGetter(obj, key, function() {
return value;
});
}
}
3.3 各Cordova 内部插件简要说明【未补充完全】
各cordova 内部的 js插件能力
cordova:1.提供callbackId/callbacks/callbackStatus变量声明【用于管理插件调用行为的标记声明】
2.提供 callbackSuccess 和 callbackError以及 callbackFromNative方法声明 分别对应:成功/失败/native调用js 能力声明
这里存在boolean变量 keepCallback 决定是否要keep住当前调用行为中使用callback(callback常驻能力的js来源)
channel: 1.提供create/createSticky等事件定义的方法
2.提供事件订阅和取消订阅能力subscribe/unSubscribe
3.提供fire能力来执行所有通过channel完成订阅的方法
【2/3里 依赖 js的prototype 原型能力,创建的具体对象获取传递的prototype 属性能力】
platform: cordova平台启动bootstrap处理
pluginloader: 提供外置js插件的注入方式,包括读取plugin声明的js数组文件并动态创建脚本
init: 1.设置加载超时事件回掉(源码设置5s)
2.添加DOMContentLoaded等事件监听
3.执行 platform 的 bootstrap,onNativeReady事件执行
4.执行pluginloader 插件中的 load方法,即加载外置的js插件(plugin_list和 bridge_list)
exec: 1.声明native和js的交互方式【android 默认对象注入(js调用native),eval_bridge(native调用js)】并提供调用api
4. Cordova功能整体流程图
sdk_interact.png4.1 js插件是如何找到native插件
以自定义插件调用过程为例:
a)h5触发事件:
// 获取经纬度信息api调用:
function getLocationGPSData(params) {
function onSuccess(result) {
navigator.notification.alert(JSON.stringify(result), null, '提示','OK');
};
function onError(error) {
navigator.notification.alert(JSON.stringify(error), null, '提示','OK');
};
XXBridge.call('executeH5InfoAction', params, onSuccess, onError);
//XXXMessage.executeH5InfoAction(params, onSuccess, onError);
}
b)代理跳转
var exec = require('cordova/exec');
//exec js 中的 export: module.exports = androidExec;exec方法和 androidExec是映射关系
XXBridge.js:
XXXMessage.executeH5InfoAction(paras, suc, err);
XXXMessage.js:
executeH5InfoAction: function(params, successCallback, errorCallback) {
exec(successCallback, errorCallback, "XXXBasicMessagePlugin", "executeH5InfoAction", [params]);
}
c)触发的事件执行代码
//对应js 触发的 exec 调用到cordova js中 注入的 exec.js androidExec 方法
function androidExec(success, fail, service, action, args) {
//bridgeSecret ->native to js module [这里默认eval]
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');
}
// Set default bridge modes if they have not already been set.
// By default, we use the failsafe, since addJavascriptInterface breaks too often
//js to native 默认是 注入JS_OBJECT方式调用
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]);
}
}
//callbackId生成 并将生成的callbackId及成功失败回调以对象存储
var callbackId = service + cordova.callbackId++,
argsJson = JSON.stringify(args);
if (success || fail) {
cordova.callbacks[callbackId] = {success:success, fail:fail};
}
//1.根据注入的调用方式完成 js到native的调用 这里exec对应android端的_cordovaNative
//2.nativeApiProvider.get() 为 nativeApi 即 _cordovaNative
//3.msgs 是 native能力实现后的返回值【无callback事件回执时为undefined】
var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);
// If argsJson was received by Java as null, try again with the PROMPT bridge mode.
// This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666.
if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") {
androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
androidExec(success, fail, service, action, args);
androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
} else if (msgs) {
//存储callback回来的msg到数组中
messagesFromNative.push(msgs);
// Always process async to avoid exceptions messing up stack.
//异步方法执行 指向前述声明的Promise.then(func) 这里func为 processMessage()
nextTick(processMessages);
}
}
//以下为nativeApi 指向注入到js的native对象说明
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;
}
};
d)回调的消息处理
function processMessages() {
// Check for the reentrant case.
if (isProcessing) {
return;
}
if (messagesFromNative.length === 0) {
return;
}
isProcessing = true;
try {
//从前面的messagesFromNative中pop出msg
var msg = popMessageFromQueue();
// The Java side can send a * message to indicate that it
// still has messages waiting to be retrieved.
if (msg == '*' && messagesFromNative.length === 0) {
nextTick(pollOnce);
return;
}
//根据native同js协定的传递消息格式处理信息
processMessage(msg);
} finally {
isProcessing = false;
if (messagesFromNative.length > 0) {
nextTick(processMessages);
}
}
}
function popMessageFromQueue() {
//从数组中移除第一项并返回
var messageBatch = messagesFromNative.shift();
if (messageBatch == '*') {
return '*';
}
//从msg中取出长度以外的信息并返回
var spaceIdx = messageBatch.indexOf(' ');
var msgLen = +messageBatch.slice(0, spaceIdx);//msg的长度
var message = messageBatch.substr(spaceIdx + 1, msgLen);
messageBatch = messageBatch.slice(spaceIdx + msgLen + 1);
if (messageBatch) {
messagesFromNative.unshift(messageBatch);
}
return message;
}
// Processes a single message, as encoded by NativeToJsMessageQueue.java.
function processMessage(message) {
var firstChar = message.charAt(0);
if (firstChar == 'J') {
// This is deprecated on the .java side. It doesn't work with CSP enabled.
eval(message.slice(1));
//S means success & F means False 对应后面方法调用的 success字段
} else if (firstChar == 'S' || firstChar == 'F') {
var success = firstChar == 'S';
var keepCallback = message.charAt(1) == '1';
var spaceIdx = message.indexOf(' ', 2);
var status = +message.slice(2, spaceIdx);
var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
//获取回执信息中的有效信息【去除标志位 插件对象名toString:callbackId】
第一位表示处理规则:第二位表示callback是否常驻:第三位表示callback的状态值用于callbackFromNative:两处空格之间为callbackId
//example:"S01 XXXGeoPlugin559127509 sOK"
var payloadMessage = message.slice(nextSpaceIdx + 1);//获取传递过来的原信息【可理解为类tcp/ip协议中的各种协议头封装后的解析过程,这里的原信息即可对应数据包】
var payload = [];
buildPayload(payload, payloadMessage);
cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
} else {
console.log("processMessage failed: invalid message: " + JSON.stringify(message));
}
}
// 执行cordova中声明的callbackFromNative,isSuccess对应前面的S/F标识位
callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) {
try {
var callback = cordova.callbacks[callbackId];
//基本逻辑是在js端存在callbackId对应的callback,成功执行success失败执行fail
if (callback) {
if (isSuccess && status == cordova.callbackStatus.OK) {
// null 或 undefined 时会自动指向全局对象 apply即执行success func
callback.success && callback.success.apply(null, args);
} else if (!isSuccess) {
callback.fail && callback.fail.apply(null, args);
}
/*
else
Note, this case is intentionally not caught.
this can happen if isSuccess is true, but callbackStatus is NO_RESULT
which is used to remove a callback from the list without calling the callbacks
typically keepCallback is false in this case
*/
// Clear callback if not expecting any more results
if (!keepCallback) {
delete cordova.callbacks[callbackId];
}
}
}
catch (err) {
var msg = "Error in " + (isSuccess ? "Success" : "Error") + " callbackId: " + callbackId + " : " + err;
console && console.log && console.log(msg);
cordova.fireWindowEvent("cordovacallbackerror", { 'message': msg });
throw err;
}
}
java代码层面对应的Status说明【对应js中传递拼接的 status字段,说明当前的action是成功还是失败以及对应状态】
public enum Status {
NO_RESULT,
OK,
CLASS_NOT_FOUND_EXCEPTION,
ILLEGAL_ACCESS_EXCEPTION,
INSTANTIATION_EXCEPTION,
MALFORMED_URL_EXCEPTION,
IO_EXCEPTION,
INVALID_ACTION,
JSON_EXCEPTION,
ERROR
}
javascript中的Status声明
callbackId: Math.floor(Math.random() * 2000000000),
callbacks: {},
callbackStatus: {
NO_RESULT: 0,
OK: 1,
CLASS_NOT_FOUND_EXCEPTION: 2,
ILLEGAL_ACCESS_EXCEPTION: 3,
INSTANTIATION_EXCEPTION: 4,
MALFORMED_URL_EXCEPTION: 5,
IO_EXCEPTION: 6,
INVALID_ACTION: 7,
JSON_EXCEPTION: 8,
ERROR: 9
}
4.2 native端代码,接收js事件 转发行为到对应子plugin 并产生回调信息传给js【eval】
a)js执行androidExec 调用到native端
[var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);]
// com.xxx.xxx.core.PluginManager#exec
public void exec(final String service, final String action, final String callbackId, final String rawArgs) {
//懒加载,根据协定传递指定key为service,初始化对应plugin【已初始化则直接取出】
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;
}
//根据js端生成的callbackId 生成 native端的 CallbackContenxt对象,这个对象是native 回调js端,传递数据的起点
CallbackContext callbackContext = new CallbackContext(callbackId, app);
try {
long pluginStartTime = System.currentTimeMillis();
// 子plugin实现execute的返回值,action对应事件名,这里通过java多态找到CordovaPlugin的子实现类【即service映射的类】并execute
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());
}
}
b)事件行为分发到对应CordovaPlugin子类
//com.xxx.xxx.core.CordovaPlugin#execute(java.lang.String, java.lang.String, com.xxx.xxx.core.CallbackContext)
public boolean execute(String action, String rawArgs, CallbackContext callbackContext) throws JSONException {
JSONArray args = new JSONArray(rawArgs);
return execute(action, args, callbackContext);
}
//com.xxx.xxx.core.CordovaPlugin#execute(java.lang.String, com.xxx.xxx.core.CordovaArgs, com.xxx.xxx.core.CallbackContext)
public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException {
mMCallbackContext = callbackContext;
return true;
}
c)子类cordovaPlugin执行并产生回调信息返回【&信息包装】
//任意CordovaPlugin子类完成事件触发即执行execute后,通过方法传递的 CallbackContext回调
...
public void success(JSONObject message) {
sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
}
...//还有很多其他重载的success方法,可以兼容传递不同类型的数据类型,最终都会在PluginResult中完成封装,转换成最终传递到js的 payload string msg
public void sendPluginResult(PluginResult pluginResult) {
synchronized (this) {
if (finished) {
LOG.w(LOG_TAG, "Attempted to send a second callback for ID: " + callbackId + "\nResult was: " + pluginResult.getMessage());
return;
} else {
//如果执行过一次的事件 【finised 为 true】 下次不会再执行,如果keepcallback则可重复调用执行,回执到js端message process后也不会被移除全局的js callback数组【所以保持了两端的同步keepcallback能力】
finished = !pluginResult.getKeepCallback();
}
}
LogUtil.d("xxx", "sendPluginResult: "+callbackId+"-----"+pluginResult );
webView.sendPluginResult(pluginResult, callbackId);
}
//com.xxx.xxx.core.CordovaWebViewImpl#sendPluginResult
@Override
public void sendPluginResult(PluginResult cr, String callbackId) {
// nativeToJsMessageQueue native生成发送到js端的消息维护类
nativeToJsMessageQueue.addPluginResult(cr, callbackId);
}
public void addPluginResult(PluginResult result, String callbackId) {
if (callbackId == null) {
LogUtil.e(LOG_TAG, "Got plugin result with no callbackId", new Throwable());
return;
}
// Don't send anything if there is no result and there is no need to
// clear the callbacks.
boolean noResult = result.getStatus() == PluginResult.Status.NO_RESULT.ordinal();
boolean keepCallback = result.getKeepCallback();
if (noResult && keepCallback) {
return;
}
// 将PluginResult 和 callbackId 封装成 JsMessage对象 [ JsMessage 是 NativeToJsMessageQueue 的内部类,JsMessage用于生成协议规则message的类]
JsMessage message = new JsMessage(result, callbackId);
if (FORCE_ENCODE_USING_EVAL) {
StringBuilder sb = new StringBuilder(message.calculateEncodedLength() + 50);
message.encodeAsJsMessage(sb);
message = new JsMessage(sb.toString());
}
enqueueMessage(message);
}
private void enqueueMessage(JsMessage message) {
synchronized (this) {
if (activeBridgeMode == null) {
LOG.d(LOG_TAG, "Dropping Native->JS message due to disabled bridge");
return;
}
queue.add(message);
if (!paused) {
activeBridgeMode.onNativeToJsMessageAvailable(this);
}
}
}
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
//这里在发送信息到js端前,先处理
String js = queue.popAndEncodeAsJs();
if (js != null) {
//最后调用 webview 的 evaluateJavascript方法,将处理过的带协议规则的信息传递到js端
engine.evaluateJavascript(js, null);
}
}
});
}
}
//完成封装的api方法
public String popAndEncodeAsJs() {
synchronized (this) {
int length = queue.size();
if (length == 0) {
return null;
}
int totalPayloadLen = 0;
int numMessagesToSend = 0;
for (JsMessage message : queue) {
//计算长度
int messageSize = message.calculateEncodedLength() + 50; // overestimate.
//MAX_PAYLOAD_SIZE最大传递信息长度
if (numMessagesToSend > 0 && totalPayloadLen + messageSize > MAX_PAYLOAD_SIZE && MAX_PAYLOAD_SIZE > 0) {
break;
}
totalPayloadLen += messageSize;
numMessagesToSend += 1;
}
boolean willSendAllMessages = numMessagesToSend == queue.size();
StringBuilder sb = new StringBuilder(totalPayloadLen + (willSendAllMessages ? 0 : 100));
// Wrap each statement in a try/finally so that if one throws it does
// not affect the next.
for (int i = 0; i < numMessagesToSend; ++i) {
//从队列中取出第一条JsMessage对象并移除
JsMessage message = queue.removeFirst();
if (willSendAllMessages && (i + 1 == numMessagesToSend)) {
message.encodeAsJsMessage(sb);
} else {
sb.append("try{");
message.encodeAsJsMessage(sb);
sb.append("}finally{");
}
}
if (!willSendAllMessages) {
sb.append("window.setTimeout(function(){cordova.require('cordova/plugin/android/polling').pollOnce();},0);");
}
for (int i = willSendAllMessages ? 1 : 0; i < numMessagesToSend; ++i) {
sb.append('}');
}
String ret = sb.toString();
return ret;
}
}
// 转换成js能够执行的message【并包装回调js端的信息】
void encodeAsJsMessage(StringBuilder sb) {
if (pluginResult == null) {
sb.append(jsPayloadOrCallbackId);
} else {
int status = pluginResult.getStatus();
boolean success = (status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal());
// js端执行 cordova#callbackFromNative 该方法去处理包装的信息
sb.append("cordova.callbackFromNative('")
.append(jsPayloadOrCallbackId)
.append("',")
.append(success)
.append(",")
.append(status)
.append(",[");
switch (pluginResult.getMessageType()) {
case PluginResult.MESSAGE_TYPE_BINARYSTRING:
sb.append("atob('")
.append(pluginResult.getMessage())
.append("')");
break;
case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
sb.append("cordova.require('cordova/base64').toArrayBuffer('")
.append(pluginResult.getMessage())
.append("')");
break;
default:
sb.append(pluginResult.getMessage());
}
sb.append("],")
.append(pluginResult.getKeepCallback())
.append(");");
}
}
// cordova.callbackFromNative('XXXMessagePlugin1365781441',true,1,["{\"latitude\":31.181945,\"longitude\":121.365825}"],false
js插件的使用和声明:
{
"id": "xxxCordova-plugin-api.XXBridge", //对应define的id
"file": "plugins/xxxCordova-plugin-api/www/XXBridge.js", //对应加载文件的path
"pluginId": "xxxCordova-plugin-api", //可无 在动态更新插件中使用
"merges": [
"XXBridge"
]
//如果是 run 则在crodova中 continue
//如果是 merge 和 clobbers 则直接挂载,其中当出现新旧对象挂载到window上时 merge标签的对应元素会出现override
}
5.对比简要说明 Cordova hybird和 ReactNative 差异
Cordova 原名phoneGap 被收购后改名为Cordova, 包括 native端 jsbridge 和 js端三部分,页面加载通过webView作为容器,通过js完成主逻辑控制,native提供原生端的能力支持
ReactNative 则包括 native runtime、 jsbridge 、js runtime,不依赖于WebView来展示页面而是使用原生控件,只不过是通过js脚本语言指定协议规则同native runtime端交互生成 映射的native 端view 添加到原生页面 窗口上
网友评论