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>() {@Overridepublic 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(){}@JavascriptInterfacepublic 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@JavascriptInterfacepublic String testSyn(Object msg)  {return msg + "[syn call]";}//异步API@JavascriptInterfacepublic 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>(){@Overridepublic 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{@JavascriptInterfacepublic String testSyn(Object msg)  {return msg + "[syn call]";}@JavascriptInterfacepublic 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 typescriptcall: 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的最后是_dsbridgeret = prompt("_dsbridge=" + method, arg); //走android  prompt}return  JSON.parse(ret||'{}').data},register: function (name, fun, asyn) {var q = asyn ? window._dsaf : window._dsfif (!window._dsInit) {window._dsInit = true;//notify native that js apis register successfully on next event loopsetTimeout(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 namespacevar 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 namespacevar 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 typescriptcall: 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的最后是_dsbridgeret = prompt("_dsbridge=" + method, arg); //走android  prompt}return  JSON.parse(ret||'{}').data},register: function (name, fun, asyn) {var q = asyn ? window._dsaf : window._dsfif (!window._dsInit) {window._dsInit = true;//notify native that js apis register successfully on next event loopsetTimeout(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参数 为 functionif (typeof args == 'function') {cb = args;// args设置为空对象args = {};}//定义 arg 对象, 包含 名为 data的属性,属性值 为 argsvar arg={data:args===undefined?null:args}// 如果cb 参数 为 functionif (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的最后是_dsbridgeret = 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 versionsettings.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 versionsettings.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@JavascriptInterfacepublic 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() {@Overridepublic void complete(Object retValue) {complete(retValue, true);}@Overridepublic void complete() {complete(null, true);}@Overridepublic 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中的

 @JavascriptInterfacepublic 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 参数 为 functionif (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() {@Overridepublic void complete(Object retValue) {complete(retValue, true);}@Overridepublic void complete() {complete(null, true);}@Overridepublic 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();}}});}
@JavascriptInterfacepublic void testAsyn(Object msg, CompletionHandler<String> handler){handler.complete(msg+" [ asyn call]");}

调用invoke()方法后,就会走到JsApi中的

 @JavascriptInterfacepublic 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 functionsdsBridge.register("syn", {tag: "syn",addValue:function (r,l) {return r+l;},getInfo: function () {return {tag: this.tag, value:8}}})// namespace test for asyn functionsdsBridge.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因为没有创建过,因此为falseif (!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,这里为 functionif (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>() {@Overridepublic 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 namespacevar 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@JavascriptInterfacepublic 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@JavascriptInterfacepublic String closePage(Object object) throws JSONException {runOnMainThread(new Runnable() {@Overridepublic void run() {if (javascriptCloseWindowListener == null|| javascriptCloseWindowListener.onClose()) {Context context = getContext();if (context instanceof Activity) {((Activity)context).onBackPressed();}}}});return null;}@Keep@JavascriptInterfacepublic void disableJavascriptDialogBlock(Object object) throws JSONException {JSONObject jsonObject = (JSONObject) object;alertBoxBlock = !jsonObject.getBoolean("disable");}@Keep@JavascriptInterfacepublic void dsinit(Object jsonObject) {DWebView.this.dispatchStartupQueue();}@Keep@JavascriptInterfacepublic void returnValue(final Object obj){Log.d("RETUREN--",obj.toString());runOnMainThread(new Runnable() {@Overridepublic 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() {@Overridepublic 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>() {@Overridepublic void onValue(Integer retValue) {showToast(retValue);}});

这里,原生端就此接收到了js回调过来的数据。

ok,至此,对DsBridge框架的基本调用原理已经全部梳理完毕,最后,做个总结:

  1. android4.4之前,原生通过loadUrl(“javascript:xxx”)的方式调js;android4.4之后,则通过evaluateJavascript()方式调js.
  2. android4.1之前,原生通过setUserAgentString()方式向js注入对象,已提供js调原生的接口;而android4.1之后,则通过addJavascriptInterface()方式向js中注入对象。
  3. dsbridge.js只是做了一些功能的封装,比如call()、register()等方法的定义,已供前端页面使用。

通过这次的梳理,个人觉得DsBridge框架其实只是在js端做了二次封装的动作,而在原生端也只是做了一个分发的动作,在call()方法中接收数据,然后通过method名称来找到对应的带有@JavascriptInterface的方法,然后回调给它。其实想想,我们自己就可以写一个类似的封装组件。

一步一步带你了解Hybrid开发框架之DsBridge相关推荐

  1. 一步一步带你理解DDR基本原理

    一步一步带你理解DDR基本原理 一.DDR概述 二.DIMM概述 三.内存颗粒内部层级结构 1.Bank.Column.Row 2.Bank Group 3.内存颗粒容量计算 四.内存颗粒的封装方式 ...

  2. 【面试必备】透过源码角度一步一步带你分析 ArrayList 扩容机制

    该文已加入开源文档:JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识).地址:https://github.com/Snailclimb/JavaGuide. 一 先从 Array ...

  3. Android中级篇之百度地图SDK v3.5.0-一步一步带你仿各大主流APP地图定位移动选址功能

    from: http://blog.csdn.net/y1scp/article/details/49095729 定位+移动选址 百学须先立志-学前须知: 我们经常在各大主流APP上要求被写上地址, ...

  4. 【面试必备】透过源码角度一步一步带你走近阿里

    一 先从 ArrayList 的构造函数说起 ArrayList有三种方式来初始化,构造方法源码如下: /** * 默认初始容量大小 */ private static final int DEFAU ...

  5. pytorch实现人脸识别_一步一步带你完成深度学习与对象检测之人脸识别

    前期文章我们分享了opencv的人脸检测 人工智能-OpenCV+Python实现人脸识别 以及dlib的人脸检测与人脸识别 人工智能-Dlib+Python实现人脸识别 通过往期的分享,我们了解到人 ...

  6. 一步一步带你训练自己的SSD检测算法

    目录 一.前言 二.实现细节 1.前提条件 2.数据标注 2.1 Labelme 2.1.1 工具特点简介 2.1.2 工具安装 2.1.3 工具使用简介 2.2 LabelImg 2.2.1 工具安 ...

  7. 一步一步带你创建Azure Point-to-Site ***

    Windows Azure 允许你将本地和云端进行打通,实现企业混合云平台的需求,如何打通本地网络和云端网络,这就需要×××技术. 而常见的×××技术主要有Point-To-Site和Site-To- ...

  8. 消费者生产者代码之---一步一步带你写

    面试时可能会面到生产者消费者模式,我们用java代码来实现下,一步一步思路给大家列出来,方便理解记忆 单生产者消费者模式 先想象一下,我们要弄一个仓库,这个仓库有两个暴露出去的方法put 和 pull ...

  9. MySQL--入门篇:MySQL入门必会知识 Windows安装MySQL的zip包 一步一步带你图解安装MySQL过程 详细图解MySQL语句

    阅读目录 数据和数据库 MySQL介绍 MySQL的详细安装教程 Windows版本:MySQLl的安装.启动和基础配置 下载 配置 环境变量 安装 和 启动MySQL服务端 MySQL的卸载 MyS ...

最新文章

  1. apache httpd 1
  2. Word for mac 分小节问题
  3. OpenCASCADE绘制测试线束:OCAF 命令之树属性命令
  4. Swagger 官方 Starter 配上这个增强方案是真的香!
  5. (67)Vue-cli 项目搭建
  6. 解析大型.NET ERP系统 高质量.NET代码设计模式
  7. 将您的SQL Server工作负载迁移到PostgreSQL –第4部分
  8. 利用python开发微信JS-JDK(基于python3.6)
  9. fw325r虚拟服务器,fw325r无线路由器设置
  10. 初始化audio失败是什么意思_DirectXAudio初始化失败,高手看 – 手机爱问
  11. Vmware虚拟机NAT模式设置IP
  12. Qt--模拟按下按键(键盘)
  13. 网页制作流程--(项目案例)学成在线
  14. gigaset812说明书_西门子+Gigaset+A280+说明书.pdf
  15. java飞鸽源码_java版本的飞鸽编写(一)
  16. 为什么HashMap链表长度超过8会转成树结构
  17. Unity游戏开发之游戏存档方式
  18. Axure RP 9的下载安装
  19. 一个例子走近 Python 的 Mixin 类:利用 Python 多继承的魔力
  20. Ubuntu下安装配置TAU分析mpich和openmp程序性能

热门文章

  1. 时刻牢记基础是关键,万丈高楼平地起靠的是什么?是坚实牢固的地基!
  2. 老生常谈之Android里的dp和sp
  3. linux内核如何支持多核cpu,现在的多核CPU,Linux操作系统是否能够实现单个进程(多线程)的多核调度(跨CPU核心调度)?...
  4. 开启VScode中最简单的内部浏览器 - 可以访问外网 - Browser Preview
  5. vue实现页面点击页面滚动-禁止鼠标滑轮滚动页面
  6. js 监听键盘的enter键
  7. 识别图片中的数字------基本思路
  8. S.M.A.R.T. 参数(smartctl)计算硬盘精确健康值
  9. 如何解决“无法连接到文件共享,因为它不安全。 此共享需要旧的 SMB1 协议”问题
  10. [Constraints 18-5210] No constraints selected for write.