Hybrid开发即 原生与前端的混合开发,常指原生+H5的混合开发。在此之前,我们来梳理下,原生与H5交互的最原始做法(这里基于android)。
android与js交互
android与js交互的核心思想是通过向页面注入javascript代码间接调用H5脚本中定义的方法。WebView为我们提供了两种注入方式,一种有回调,一种无回调。
- SDK<19,android提供的无回调方法:loadUrl(“javascript:xxx”)
- SDK>=19,android新增了有回调的方法:evaluateJavascript(String,ValueCallback)
比如H5页面中定义了这样一个方法:
<script type="">
function show() {
alert("show()---");
return "js接收到了消息---";
}
</script>
那么用上面的方法可以这样调用:
String scrpit = "show()";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webView.evaluateJavascript(script, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
} else {
webView.loadUrl("javascript:" + script);
}
##js与android交互
js与android交互的核心思想是 android向页面注入一个对象后,js就可以拿到android注入进来的对象,通过这个对象调用其方法。
核心代码:
WebSettings webSettings = wv.getSettings();
webSettings.setJavaScriptEnabled(true);
wv.addJavascriptInterface(new JsApp(),"app");
class JsApp{
public JsApp(){}
@JavascriptInterface
public void call(Object obj){
}
}
这里,我们向页面注入了JsApp对象,命名为app,这样H5就可以通过app来调用JsApp中的方法了,H5中调用方式:
app.call(obj);
我们发现了JsApp中call()加了@JavascriptInterface注解,这是android4.2之后引入的。因为webview允许JavaScript 控制宿主应用程序,这是个很强大的特性,但同时,在4.2的版本前存在重大安全隐患,因为JavaScript 可以使用反射访问注入webview的java对象的public fields,在一个包含不信任内容的WebView中使用这个方法,会允许攻击者去篡改宿主应用程序,使用宿主应用程序的权限执行java代码。因此4.2以后,任何为JS暴露的接口,都需要加@@JavascriptInterface注解,这样可以避免Java对象的fields 被JS访问。
##Hybrid开发框架-DsBridge
基于前面我们讲到的android与js的交互,一些方便我们进行hybrid开发的框架应运而生,比如下面我们要分析的DsBridge框架。
###使用方式
1.添加 JitPack repository 到gradle脚本中
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
2.添加依赖
dependencies {
//compile 'com.github.wendux:DSBridge-Android:3.0-SNAPSHOT'
//support the x5 browser core of tencent
//compile 'com.github.wendux:DSBridge-Android:x5-3.0-SNAPSHOT'
}
3.新建一个java类,实现API
public class JsApi{
//同步API
@JavascriptInterface
public String testSyn(Object msg) {
return msg + "[syn call]";
}
//异步API
@JavascriptInterface
public void testAsyn(Object msg, CompletionHandler<String> handler) {
handler.complete(msg+" [ asyn call]");
}
}
可以看到,DSBridge正式通过类的方式集中、统一地管理API。由于安全原因,所有Java API 必须有"@JavascriptInterface" 标注。
4.添加API类实例到 DWebView
DWebView dwebView= (DWebView) findViewById(R.id.dwebview);
dwebView.addJavascriptObject(new JsApi(), null);
5.在Javascript中调用原生 (Java/Object-c/swift) API ,并注册一个 javascript API供原生调用
js调原生:
//同步调用
dsBridge.call("testSyn","testSyn");
//异步调用
dsBridge.call("testAsyn","testAsyn", function (v) {
alert(v);
})
//注册 javascript API
dsBridge.register('addValue',function(l,r){
return l+r;
})
6.在Java中调用 Javascript API
dwebView.callHandler("addValue",new Object[]{3,4},new OnReturnValue<Integer>(){
@Override
public void onValue(Integer retValue) {
Log.d("jsbridge","call succeed,return value is "+retValue);
}
});
以上使用方法来自github,接下来,我们就针对js调原生、原生调js两个方面来疏通其流程。
###js调原生
打开github上提供的DSBridge-Android工程,在/src/main/assets下存放着前端需要用到的资源,其中我们主要关注js-call-native.html、dsbridge.js。前者是js调原生的H5页面,后者是Node.js写的js模块,主要是提供给前者调用的功能封装。
js-call-native.html:
<!DOCTYPE html>
<html>
<head lang="zh-cmn-Hans">
<meta charset="UTF-8">
<title>DSBridge Test</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<meta name="viewport" content="width=device-width,initial-scale=0.5,user-scalable=no"/>
<!--require dsbridge init js-->
<script src="./dsbridge.js"> </script>
</head>
<body>
<div class="btn" onclick="callSyn()">Synchronous call</div>
<div class="btn" onclick="callAsyn()">Asynchronous call</div>
...
<script>
function callSyn() {
alert(dsBridge.call("testSyn", "testSyn"))
}
function callAsyn() {
dsBridge.call("testAsyn","testAsyn", function (v) {
alert(v)
})
}
function callAsyn_() {
for (var i = 0; i < 2000; i++) {
dsBridge.call("testAsyn", "js+" + i, function (v) {
if (v == "js+1999 [ asyn call]") {
alert("All tasks completed!")
}
})
}
}
//省略了其他的代码,我们只看这两个方法
...
</script>
</body>
</html>
JsApi.java:
public class JsApi{
@JavascriptInterface
public String testSyn(Object msg) {
return msg + "[syn call]";
}
@JavascriptInterface
public void testAsyn(Object msg, CompletionHandler<String> handler){
handler.complete(msg+" [ asyn call]");
}
//省略了其他代码,我们只看着两个方法
...
}
我们看这里:
function callSyn() {
alert(dsBridge.call("testSyn", "testSyn"))
}
callSyn();
通过调用callSyn()方法,js就可以调用到android中定义在JsApi.java中testSyn()方法。
callSyn()方法中,走了dsBridge.call(),它是哪里来的呢?这时我们就要关注前面提到的dsBridge.js了,通过<script src="./dsbridge.js"> </script>将dsbridge.js注入到当前页面,这样就可以调用dsbridge模块了。接下来,我们来看dsbridge.js:
var bridge = {
default:this,// for typescript
call: function (method, args, cb) {
var ret = '';
if (typeof args == 'function') {
cb = args;
args = {};
}
var arg={data:args===undefined?null:args}//定义arg对象
if (typeof cb == 'function') { //如果cb参数是一个方法
var cbName = 'dscb' + window.dscb++; //cbName = dscb1、dscb2、dscb3...
window[cbName] = cb; //window.dscb1 = cb,因此可以调用 dscb1(v)、dscb2(v)等函数,对应 function callAsyn() {
// dsBridge.call("testAsyn","testAsyn", function (v) {
// alert(v)
// })
// } 中的 function(v){}
arg['_dscbstub'] = cbName; //arg对象中添加一个属性 _dscbstub = cbName
}
arg = JSON.stringify(arg) //arg转为json
//if in webview that dsBridge provided, call!
if(window._dsbridge){//是否注入过 _dsbridge对象(android注入的对象)
ret= _dsbridge.call(method, arg) //调用android对象的call()
}else if(window._dswk||navigator.userAgent.indexOf("_dsbridge")!=-1){//如果注入过_dswk对象,或者 userAgent的最后是_dsbridge
ret = prompt("_dsbridge=" + method, arg); //走android prompt
}
return JSON.parse(ret||'{}').data
},
register: function (name, fun, asyn) {
var q = asyn ? window._dsaf : window._dsf
if (!window._dsInit) {
window._dsInit = true;
//notify native that js apis register successfully on next event loop
setTimeout(function () {
bridge.call("_dsb.dsinit");
}, 0)
}
if (typeof fun == "object") {
q._obs[name] = fun;
} else {
q[name] = fun
}
},
registerAsyn: function (name, fun) {
this.register(name, fun, true);
},
hasNativeMethod: function (name, type) {
return this.call("_dsb.hasNativeMethod", {name: name, type:type||"all"});
},
disableJavascriptDialogBlock: function (disable) {
this.call("_dsb.disableJavascriptDialogBlock", {
disable: disable !== false
})
}
};
!function () {
if (window._dsf) return;
var _close=window.close;
var ob = {
//保存JS同步方法
_dsf: {
_obs: {}
},
//保存JS异步方法
_dsaf: {
_obs: {}
},
dscb: 0,
dsBridge: bridge,
close: function () {
if(bridge.hasNativeMethod('_dsb.closePage')){
bridge.call("_dsb.closePage")
}else{
_close.call(window)
}
},
_handleMessageFromNative: function (info) {
var arg = JSON.parse(info.data);
var ret = {
id: info.callbackId,
complete: true
}
var f = this._dsf[info.method];
var af = this._dsaf[info.method]
var callSyn = function (f, ob) {
ret.data = f.apply(ob, arg)
bridge.call("_dsb.returnValue", ret)
}
var callAsyn = function (f, ob) {
arg.push(function (data, complete) {
ret.data = data;
ret.complete = complete!==false;
bridge.call("_dsb.returnValue", ret)
})
f.apply(ob, arg)
}
if (f) {
callSyn(f, this._dsf);
} else if (af) {
callAsyn(af, this._dsaf);
} else {
//with namespace
var name = info.method.split('.');
if (name.length<2) return;
var method=name.pop();
var namespace=name.join('.')
var obs = this._dsf._obs;
var ob = obs[namespace] || {};
var m = ob[method];
if (m && typeof m == "function") {
callSyn(m, ob);
return;
}
obs = this._dsaf._obs;
ob = obs[namespace] || {};
m = ob[method];
if (m && typeof m == "function") {
callAsyn(m, ob);
return;
}
}
}
}
for (var attr in ob) {
window[attr] = ob[attr]
}
bridge.register("_hasJavascriptMethod", function (method, tag) {
var name = method.split('.')
if(name.length<2) {
return !!(_dsf[name]||_dsaf[name])
}else{
// with namespace
var method=name.pop()
var namespace=name.join('.')
var ob=_dsf._obs[namespace]||_dsaf._obs[namespace]
return ob&&!!ob[method]
}
})
}();
module.exports = bridge;
dsBridge.js中首先会走下面的匿名函数function(),这里它没有用js标准自调用函数的写法:
(function(){})() 或者 (function(){}())
而是用!function () {}()这种方式来实现自调用,区别在于这样会有返回值。这个方法有这么一句声明:dsBridge: bridge,知道为啥前面的H5页面可以直接通过dsBridge.call()来调用dsBridge.js中的call方法了吧,这里将dsBridge指向了bridge组件,bridge是什么呢?
var bridge = {
default:this,// for typescript
call: function (method, args, cb) {
var ret = '';
if (typeof args == 'function') {
cb = args;
args = {};
}
var arg={data:args===undefined?null:args}//定义arg对象
if (typeof cb == 'function') { //如果cb参数是一个方法
var cbName = 'dscb' + window.dscb++; //cbName = dscb1、dscb2、dscb3...
window[cbName] = cb; //window.dscb1 = cb,因此可以调用 dscb1(v)、dscb2(v)等函数,对应 function callAsyn() {
// dsBridge.call("testAsyn","testAsyn", function (v) {
// alert(v)
// })
// } 中的 function(v){}
arg['_dscbstub'] = cbName; //arg对象中添加一个属性 _dscbstub = cbName
}
arg = JSON.stringify(arg) //arg转为json
//if in webview that dsBridge provided, call!
if(window._dsbridge){//是否注入过 _dsbridge对象(android注入的对象)
ret= _dsbridge.call(method, arg) //调用android对象的call()
}else if(window._dswk||navigator.userAgent.indexOf("_dsbridge")!=-1){//如果注入过_dswk对象,或者 userAgent的最后是_dsbridge
ret = prompt("_dsbridge=" + method, arg); //走android prompt
}
return JSON.parse(ret||'{}').data
},
register: function (name, fun, asyn) {
var q = asyn ? window._dsaf : window._dsf
if (!window._dsInit) {
window._dsInit = true;
//notify native that js apis register successfully on next event loop
setTimeout(function () {
bridge.call("_dsb.dsinit");
}, 0)
}
if (typeof fun == "object") {
q._obs[name] = fun;
} else {
q[name] = fun
}
},
registerAsyn: function (name, fun) {
this.register(name, fun, true);
},
hasNativeMethod: function (name, type) {
return this.call("_dsb.hasNativeMethod", {name: name, type:type||"all"});
},
disableJavascriptDialogBlock: function (disable) {
this.call("_dsb.disableJavascriptDialogBlock", {
disable: disable !== false
})
}
};
module.exports = bridge;
原来bridge就是H5页面最终要调用的方法封装模块,通过module.exports = bridge;对外暴露exports引用接口,其他模块就可以通过require语句来引用这个模块,但这里我们是直接通过<script src="./dsbridge.js"> </script>标签引入。
接下来着重分析bridge中的各个方法,首先来看call():
//定义名为 call 的方法 function call(method, args, cb)
call: function (method, args, cb) {
var ret = '';
//如果 args参数 为 function
if (typeof args == 'function') {
cb = args;
// args设置为空对象
args = {};
}
//定义 arg 对象, 包含 名为 data的属性,属性值 为 args
var arg={data:args===undefined?null:args}
// 如果cb 参数 为 function
if (typeof cb == 'function') {
// cbName = dscb1、dscb2、dscb3...
var cbName = 'dscb' + window.dscb++;
// 定义 cbName(dscb1、dscb2、dscb3... )为全局方法 cnName(),其对应的就是cb(cb传入的如果是一个方法的话)
window[cbName] = cb; //window.dscb1 = cb,因此可以调用 dscb1(v)、dscb2(v)等函数,对应 function callAsyn() {
// dsBridge.call("testAsyn","testAsyn", function (v) {
// alert(v)
// })
// } 中的 function(v){}
arg['_dscbstub'] = cbName; //arg对象中添加一个属性 _dscbstub = cbName
}
arg = JSON.stringify(arg) //arg转为json
//if in webview that dsBridge provided, call!
if(window._dsbridge){//是否注入过 _dsbridge对象(android注入的对象,通过addJavascriptInterface(obj,string)方法注入,对应InnerJavascriptInterface)
ret= _dsbridge.call(method, arg) //调用android对象的call(),这里是方法调用最终点,通过这里才实现了 js->android的调用
}else if(window._dswk||navigator.userAgent.indexOf("_dsbridge")!=-1){//如果注入过_dswk对象,或者 userAgent的最后是_dsbridge
ret = prompt("_dsbridge=" + method, arg); //走android prompt模式交互(WebChromClient.onJsPrompt())
}
// 返回 _dsbridge.call(method, arg) 回调的值
return JSON.parse(ret||'{}').data
}
通过注释说明,我们可以知道,H5通过调用callSyn()实际上是调用了dsbridge.js中的call()方法,最终通过_dsbridge.cal(method,arg)将数据传递到android端。
接下来,我们就将目光转向android端,看它是如何将js传过来的数据做分发处理的。
我们看DWebView的init()方法
private void init() {
APP_CACHE_DIRNAME = getContext().getFilesDir().getAbsolutePath() + "/webcache";
WebSettings settings = getSettings();
settings.setDomStorageEnabled(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().setAcceptThirdPartyCookies(this, true);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
settings.setAllowFileAccess(false);
settings.setAppCacheEnabled(false);
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setJavaScriptEnabled(true);
settings.setLoadWithOverviewMode(true);
settings.setAppCachePath(APP_CACHE_DIRNAME);
settings.setUseWideViewPort(true);
super.setWebChromeClient(mWebChromeClient);
addInternalJavascriptObject();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {//android4.1之后,Js 使用 对象 注入方式与android交互
super.addJavascriptInterface(innerJavascriptInterface, BRIDGE_NAME);
} else {//android4.1之前 Js 使用 WebChromClient.onJsPromt()方式与android交互
// add dsbridge tag in lower android version
settings.setUserAgentString(settings.getUserAgentString() + " _dsbridge");
}
}
看到这里
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {//android4.1之后,Js 使用 对象 注入方式与android交互
super.addJavascriptInterface(innerJavascriptInterface, BRIDGE_NAME);
} else {//android4.1之前 Js 使用 WebChromClient.onJsPromt()方式与android交互
// add dsbridge tag in lower android version
settings.setUserAgentString(settings.getUserAgentString() + " _dsbridge");
}
4.1之后,我们向页面注入了InnerJavascriptInterface对象,名为 BRIDGE_NAME = “_dsbridge”,这与我们之前分析的一致。
InnerJavascriptInterface:
private class InnerJavascriptInterface {
private void PrintDebugInfo(String error) {
Log.d(LOG_TAG, error);
if (isDebug) {
evaluateJavascript(String.format("alert('%s')", "DEBUG ERR MSG:\\n" + error.replaceAll("\\'", "\\\\'")));
}
}
@Keep
@JavascriptInterface
public String call(String methodName, String argStr) {
Log.d("Call-->",methodName+"-"+argStr);
String error = "Js bridge called, but can't find a corresponded " +
"JavascriptInterface object , please check your code!";
String[] nameStr = parseNamespace(methodName.trim());
methodName = nameStr[1];
Object jsb = javaScriptNamespaceInterfaces.get(nameStr[0]);
JSONObject ret = new JSONObject();
try {
ret.put("code", -1);
} catch (JSONException e) {
e.printStackTrace();
}
if (jsb == null) {
PrintDebugInfo(error);
return ret.toString();
}
Object arg=null;
Method method = null;
String callback = null;
try {
JSONObject args = new JSONObject(argStr);
if (args.has("_dscbstub")) {
callback = args.getString("_dscbstub");
}
if(args.has("data")) {
arg = args.get("data");
}
} catch (JSONException e) {
error = String.format("The argument of \"%s\" must be a JSON object string!", methodName);
PrintDebugInfo(error);
e.printStackTrace();
return ret.toString();
}
Class<?> cls = jsb.getClass();
boolean asyn = false;
try {
method = cls.getMethod(methodName,
new Class[]{Object.class, CompletionHandler.class});
asyn = true;
} catch (Exception e) {
try {
method = cls.getMethod(methodName, new Class[]{Object.class});
} catch (Exception ex) {
}
}
if (method == null) {
error = "Not find method \"" + methodName + "\" implementation! please check if the signature or namespace of the method is right ";
PrintDebugInfo(error);
return ret.toString();
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
//检查方法是否有@JavascriptInterface注解
JavascriptInterface annotation = method.getAnnotation(JavascriptInterface.class);
if (annotation == null) {
error = "Method " + methodName + " is not invoked, since " +
"it is not declared with JavascriptInterface annotation! ";
PrintDebugInfo(error);
return ret.toString();
}
}
Object retData;
method.setAccessible(true);
try {
if (asyn) {
final String cb = callback;
method.invoke(jsb, arg, new CompletionHandler() {
@Override
public void complete(Object retValue) {
complete(retValue, true);
}
@Override
public void complete() {
complete(null, true);
}
@Override
public void setProgressData(Object value) {
complete(value, false);
}
private void complete(Object retValue, boolean complete) {
try {
JSONObject ret = new JSONObject();
ret.put("code", 0);
ret.put("data", retValue);
//retValue = URLEncoder.encode(ret.toString(), "UTF-8").replaceAll("\\+", "%20");
if (cb != null) {
//String script = String.format("%s(JSON.parse(decodeURIComponent(\"%s\")).data);", cb, retValue);
String script = String.format("%s(%s.data);", cb, ret.toString());
if (complete) {
script += "delete window." + cb;
}
//Log.d(LOG_TAG, "complete " + script);
evaluateJavascript(script);
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
} else {
retData = method.invoke(jsb, arg);
ret.put("code", 0);
ret.put("data", retData);
return ret.toString();
}
} catch (Exception e) {
e.printStackTrace();
error = String.format("Call failed:The parameter of \"%s\" in Java is invalid.", methodName);
PrintDebugInfo(error);
return ret.toString();
}
return ret.toString();
}
}
call()接收到js传过来的methodName、argStr两个参数,通过methodName匹配Map结合得到存入的JsApi对象,然后通过反射得到methodName对应的JsApi方法,然后判断其方法是否有@JavascriptInterface 注解修饰,如果没有,则返回错误信息。如果有,则通过invoke()传入argStr执行JsApi的methodName对应的方法,从前面我们知道这里的methodName=“testSyn”,对应JsApi中的
@JavascriptInterface
public String testSyn(Object msg) {
return msg + "[syn call]";
}
走到这儿,JsApi的testSyn方法就会执行,整个链路就完成了。
testAsyn()方法相较testSyn()多了一个回调接口,它整体流程其实跟testSyn()差不多,区别在于
function callAsyn() {
dsBridge.call("testAsyn","testAsyn", function (v) {
alert(v)
})
}
这里传入的第三个参数是function类型,因此在调用dsbridge.js中的call()方法时,会走
// 如果cb 参数 为 function
if (typeof cb == 'function') {
// cbName = dscb1、dscb2、dscb3...
var cbName = 'dscb' + window.dscb++;
// 定义 cbName(dscb1、dscb2、dscb3... )为全局方法 cnName(),其对应的就是cb(cb传入的如果是一个方法的话)
window[cbName] = cb; //window.dscb1 = cb,因此可以调用 dscb1(v)、dscb2(v)等函数,对应 function callAsyn() {
// dsBridge.call("testAsyn","testAsyn", function (v) {
// alert(v)
// })
// } 中的 function(v){}
arg['_dscbstub'] = cbName; //arg对象中添加一个属性 _dscbstub = cbName
}
这段代码,这段代码定义了名为 dscb1(v)、dscb2(v)…的函数,并且在arg中添加了_dscbstub这个属性,当callAsyn()最终走到InnerJavascriptInterface的call()方法时,这里就会有所不同,它会走
if (args.has("_dscbstub")) {
callback = args.getString("_dscbstub");//dscb1、dscb2、dscb3...
}
然后走到
try {
method = cls.getMethod(methodName,
new Class[]{Object.class, CompletionHandler.class});
asyn = true;
}
最后会走到:
if (asyn) {
final String cb = callback;
method.invoke(jsb, arg, new CompletionHandler() {
@Override
public void complete(Object retValue) {
complete(retValue, true);
}
@Override
public void complete() {
complete(null, true);
}
@Override
public void setProgressData(Object value) {
complete(value, false);
}
private void complete(Object retValue, boolean complete) {
try {
JSONObject ret = new JSONObject();
ret.put("code", 0);
ret.put("data", retValue);
//retValue = URLEncoder.encode(ret.toString(), "UTF-8").replaceAll("\\+", "%20");
if (cb != null) {//dscb1、dscb2、dscb3...
//String script = String.format("%s(JSON.parse(decodeURIComponent(\"%s\")).data);", cb, retValue);
String script = String.format("%s(%s.data);", cb, ret.toString());
if (complete) {
script += "delete window." + cb;
}
//Log.d(LOG_TAG, "complete " + script);
evaluateJavascript(script);
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
@JavascriptInterface
public void testAsyn(Object msg, CompletionHandler<String> handler){
handler.complete(msg+" [ asyn call]");
}
调用invoke()方法后,就会走到JsApi中的
@JavascriptInterface
public void testAsyn(Object msg, CompletionHandler<String> handler){
handler.complete(msg+" [ asyn call]");
}
这个方法,这里android端收到js传过来的消息msg,但这里,链路并没有结束,而是通过handler.complele()将android端消息回传过去了,我们看这里:
private void complete(Object retValue, boolean complete) {
try {
JSONObject ret = new JSONObject();
ret.put("code", 0);
ret.put("data", retValue);
//retValue = URLEncoder.encode(ret.toString(), "UTF-8").replaceAll("\\+", "%20");
if (cb != null) {
//String script = String.format("%s(JSON.parse(decodeURIComponent(\"%s\")).data);", cb, retValue);
String script = String.format("%s(%s.data);", cb, ret.toString());
if (complete) {
script += "delete window." + cb;
}
//Log.d(LOG_TAG, "complete " + script);
evaluateJavascript(script);
}
} catch (Exception e) {
e.printStackTrace();
}
}
原来,最终是通过evaluateJavascript(script);将消息回传给js。而回传调用的js方法正是在dsbridge.js中定义的方法:
// cbName = dscb1、dscb2、dscb3...
var cbName = 'dscb' + window.dscb++;
// 定义 cbName(dscb1、dscb2、dscb3... )为全局方法 cnName(),其对应的就是cb(cb传入的如果是一个方法的话)
window[cbName] = cb; //window.dscb1 = cb,因此可以调用 dscb1(v)、dscb2(v)等函数,对应 function callAsyn() {
// dsBridge.call("testAsyn","testAsyn", function (v) {
// alert(v)
// })
到这里,js调android带回调方法的链路全部走完。
上面我们已经把js调android的过程全部梳理完,接下来,就是对android调js的过程进行解析了。
###原生调js
还是看github提供的demo工程DSBridge-Android,我们只看 native-call-js.html和CallJavascriptActivity.
先看native-call-js.html中定义了什么:
<script>
dsBridge.register('addValue', function (r, l) {
return r + l;
})
dsBridge.registerAsyn('append', function (arg1, arg2, arg3, responseCallback) {
responseCallback(arg1 + " " + arg2 + " " + arg3);
})
dsBridge.registerAsyn('startTimer', function (responseCallback) {
var t = 0;
var timer = setInterval(function () {
if (t == 5) {
responseCallback(t)
clearInterval(timer)
} else {
// if the 2nd argument is false, the java callback handler will be not removed!
responseCallback(t++, false)
}
}, 1000)
})
// namespace test for syn functions
dsBridge.register("syn", {
tag: "syn",
addValue:function (r,l) {
return r+l;
},
getInfo: function () {
return {tag: this.tag, value:8}
}
})
// namespace test for asyn functions
dsBridge.registerAsyn("asyn", {
tag: "asyn",
addValue:function (r,l, responseCallback) {
responseCallback(r+l);
},
getInfo: function (responseCallback) {
responseCallback({tag: this.tag, value:8})
}
})
</script>
可以看到,js脚本中调用了dsBridge的register()方法,我们看看这个register():
register: function (name, fun, asyn) {
//如果没传asyn,则 q 为 false ,初始化 _dsf对象;反之创建_dsaf对象
var q = asyn ? window._dsaf : window._dsf
//window._dsInit因为没有创建过,因此为false
if (!window._dsInit) {
window._dsInit = true;
//notify native that js apis register successfully on next event loop
// 0s后执行 function()中的代码
setTimeout(function () {
//调用call方法,只传 _dsb.dsinit这个参数,其实就是将这个字段传给原生,告知js这边初始化了
bridge.call("_dsb.dsinit");
}, 0)
}
//如果 fun 为 objetct,这里为 function
if (typeof fun == "object") {
q._obs[name] = fun;
} else {
// window._dsf["addValue"] = function(r,l){return r+l}
q[name] = fun
}
}
首先判断 asyn的值,我们因为没传这个值,因此为false,所以var q = window._dsf. 那_dsf又是什么呢?在!function(){}中有定义
var ob = {
//保存JS同步方法
_dsf: {
_obs: {}
},
//保存JS异步方法
_dsaf: {
_obs: {}
},
...
}
原来dsBridge.js在被调用的时候就会初始化_dsf/_dsaf对象。
继续回到register方法中,接下来会判断window._dsInit,这里也为false,因此会走到方法体中,在0s后执行call()方法,其实就是通知原生端js初始化过程。
接着,判断fun是否为object,这里fun是function类型,因此会走q[name] = fun,其实就相当于
_dsf:{
addValue:function(r,l){r+l}
}
在_dsf对象中添加了addValue属性,它是一个function。这里为后面做调用addValue()做铺垫。
addValue(r,l)
好了,到这儿,我们把目光转向原生这边,demo中原生调js是这样调的:
dWebView.callHandler("addValue", new Object[]{3, 4}, new OnReturnValue<Integer>() {
@Override
public void onValue(Integer retValue) {
showToast(retValue);
}
});
走到callHandler()中:
public synchronized <T> void callHandler(String method, Object[] args, final OnReturnValue<T> handler) {
CallInfo callInfo = new CallInfo(method, ++callID, args);
if (handler != null) {
handlerMap.put(callInfo.callbackId, handler);
}
if (callInfoList != null) {
callInfoList.add(callInfo);
} else {
dispatchJavascriptCall(callInfo);
}
}
因为callInfoList == null,因此会走dispatchJavascriptCall(callInfo),继续进入该方法:
private void dispatchJavascriptCall(CallInfo info) {
evaluateJavascript(String.format("window._handleMessageFromNative(%s)", info.toString()));
}
它调用了
evaluateJavascript(String.format("window._handleMessageFromNative(%s)", info.toString()));
这里为: window._handleMessageFromNative{“method”:“addValue”,“callbackId”:1,“data”:"[3,4]"}。
最终,这个方法会走到:
private void _evaluateJavascript(String script) {
Log.d("SCRPIT-===",script);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
DWebView.super.evaluateJavascript(script, null);
} else {
super.loadUrl("javascript:" + script);
}
}
到这里,我们知道了,android4.4之前原生是通过loadUrl()的方式与js交互;android4.4之后是通过evaluateJavascript()方式与js交互。
现在的问题是,原生window._handleMessageFromNative{"method":"addValue","callbackId":1,"data":"[3,4]"}发送发送给js后,js是如何识别的呢?
这里,我们又得把目光转向dsbridge.js了,在dsbridge.js中,!function()其实开始的时候就已经定义了_handleMessageFromNative方法
_handleMessageFromNative: function (info) {
var arg = JSON.parse(info.data);
var ret = {
id: info.callbackId,
complete: true
}
//_dsf对象中增加 addValue属性
var f = this._dsf[info.method];
//_dsaf对象中增加addValue属性
var af = this._dsaf[info.method]
// callSyn(f,ob)
var callSyn = function (f, ob) {
ret.data = f.apply(ob, arg)
bridge.call("_dsb.returnValue", ret)
}
var callAsyn = function (f, ob) {
arg.push(function (data, complete) {
ret.data = data;
ret.complete = complete!==false;
bridge.call("_dsb.returnValue", ret)
})
f.apply(ob, arg)
}
if (f) {
callSyn(f, this._dsf);
} else if (af) {
callAsyn(af, this._dsaf);
} else {
//with namespace
var name = info.method.split('.');
if (name.length<2) return;
var method=name.pop();
var namespace=name.join('.')
var obs = this._dsf._obs;
var ob = obs[namespace] || {};
var m = ob[method];
if (m && typeof m == "function") {
callSyn(m, ob);
return;
}
obs = this._dsaf._obs;
ob = obs[namespace] || {};
m = ob[method];
if (m && typeof m == "function") {
callAsyn(m, ob);
return;
}
}
}
这里f为true,因此会走call(f,this._dsf),相当于是
_dsf: {
_obs: {},
addValue:function(r,l){
return r+l;
}
}
var f = _dsf["addValue"]
callSyn(f,this._dsf) -> function(_dsf["addValue"],this._dsf)
而_dsf[“adValue”]对应的是一个function,因此会执行
addValue(r,l)
它会将 r+l 作为返回值返回,接着会执行
ret.data = f.apply(ob, arg)
bridge.call("_dsb.returnValue", ret)
ret.data其实就是得到addValue(r,l)的返回值,最后通过bridge.call("_dsb.returnValue", ret)将消息发送给原生。
接着原生这边就会在DWebView.call()方法中接收消息,接着又会走
String[] nameStr = parseNamespace(methodName.trim());
methodName = nameStr[1];
得到methodName = “returnValue”,然后走:
Object jsb = javaScriptNamespaceInterfaces.get(nameStr[0]);//_dsb
JSONObject ret = new JSONObject();
这里会在javaScriptNamespaceInterfaces集合中通过key = “_dsb”来查找到注入到js中的对象,那_dsb是在哪里注入的呢?我们看DWebView.init()中 addInternalJavascriptObject();
addJavascriptObject(new Object() {
@Keep
@JavascriptInterface
public boolean hasNativeMethod(Object args) throws JSONException {
JSONObject jsonObject = (JSONObject) args;
String methodName = jsonObject.getString("name").trim();
String type = jsonObject.getString("type").trim();
String[] nameStr = parseNamespace(methodName);
Object jsb = javaScriptNamespaceInterfaces.get(nameStr[0]);
if (jsb != null) {
Class<?> cls = jsb.getClass();
boolean asyn = false;
Method method = null;
try {
method = cls.getMethod(nameStr[1],
new Class[]{Object.class, CompletionHandler.class});
asyn = true;
} catch (Exception e) {
try {
method = cls.getMethod(nameStr[1], new Class[]{Object.class});
} catch (Exception ex) {
}
}
if (method != null) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
JavascriptInterface annotation = method.getAnnotation(JavascriptInterface.class);
if (annotation == null) {
return false;
}
}
if ("all".equals(type) || (asyn && "asyn".equals(type) || (!asyn && "syn".equals(type)))) {
return true;
}
}
}
return false;
}
@Keep
@JavascriptInterface
public String closePage(Object object) throws JSONException {
runOnMainThread(new Runnable() {
@Override
public void run() {
if (javascriptCloseWindowListener == null
|| javascriptCloseWindowListener.onClose()) {
Context context = getContext();
if (context instanceof Activity) {
((Activity)context).onBackPressed();
}
}
}
});
return null;
}
@Keep
@JavascriptInterface
public void disableJavascriptDialogBlock(Object object) throws JSONException {
JSONObject jsonObject = (JSONObject) object;
alertBoxBlock = !jsonObject.getBoolean("disable");
}
@Keep
@JavascriptInterface
public void dsinit(Object jsonObject) {
DWebView.this.dispatchStartupQueue();
}
@Keep
@JavascriptInterface
public void returnValue(final Object obj){
Log.d("RETUREN--",obj.toString());
runOnMainThread(new Runnable() {
@Override
public void run() {
JSONObject jsonObject = (JSONObject) obj;
Object data = null;
try {
int id = jsonObject.getInt("id");
boolean isCompleted = jsonObject.getBoolean("complete");
OnReturnValue handler = handlerMap.get(id);
if (jsonObject.has("data")) {
data = jsonObject.get("data");
}
if (handler != null) {
handler.onValue(data);
if (isCompleted) {
handlerMap.remove(id);
}
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
}, "_dsb");
看到没,原来是在init()方法中就已经默认注入了名为_dsb的Object对象。
接着,因为前面得到methodName=“returnValue”,因此接下来会通过反射找到_dsb所对应的对象的returnValue方法,然后将数据回调给这个方法,我们看returnValue()
runOnMainThread(new Runnable() {
@Override
public void run() {
JSONObject jsonObject = (JSONObject) obj;
Object data = null;
try {
int id = jsonObject.getInt("id");
boolean isCompleted = jsonObject.getBoolean("complete");
OnReturnValue handler = handlerMap.get(id);
if (jsonObject.has("data")) {
data = jsonObject.get("data");
}
if (handler != null) {
handler.onValue(data);
if (isCompleted) {
handlerMap.remove(id);
}
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
到这里,我们就清楚了,最终通过handler.onValue(data)将数据回到
dWebView.callHandler("addValue", new Object[]{3, 4}, new OnReturnValue<Integer>() {
@Override
public void onValue(Integer retValue) {
showToast(retValue);
}
});
这里,原生端就此接收到了js回调过来的数据。
ok,至此,对DsBridge框架的基本调用原理已经全部梳理完毕,最后,做个总结:
- android4.4之前,原生通过loadUrl(“javascript:xxx”)的方式调js;android4.4之后,则通过evaluateJavascript()方式调js.
- android4.1之前,原生通过setUserAgentString()方式向js注入对象,已提供js调原生的接口;而android4.1之后,则通过addJavascriptInterface()方式向js中注入对象。
- dsbridge.js只是做了一些功能的封装,比如call()、register()等方法的定义,已供前端页面使用。
通过这次的梳理,个人觉得DsBridge框架其实只是在js端做了二次封装的动作,而在原生端也只是做了一个分发的动作,在call()方法中接收数据,然后通过method名称来找到对应的带有@JavascriptInterface的方法,然后回调给它。其实想想,我们自己就可以写一个类似的封装组件。
本文详细介绍了Hybrid开发中Android与H5的交互方式,包括无回调的loadUrl和有回调的evaluateJavascript。接着,文章探讨了js与android交互的原理,强调了@JavascriptInterface注解的重要性。然后,文章重点分析了DsBridge框架的使用方法和工作流程,包括js调用原生、原生调用js的细节。最后,作者指出DsBridge主要是对原生和js交互进行了封装,简化了Hybrid开发中的接口调用。

3260

被折叠的 条评论
为什么被折叠?



