一步一步带你了解Hybrid开发框架之DsBridge

本文详细介绍了Hybrid开发中Android与H5的交互方式,包括无回调的loadUrl和有回调的evaluateJavascript。接着,文章探讨了js与android交互的原理,强调了@JavascriptInterface注解的重要性。然后,文章重点分析了DsBridge框架的使用方法和工作流程,包括js调用原生、原生调用js的细节。最后,作者指出DsBridge主要是对原生和js交互进行了封装,简化了Hybrid开发中的接口调用。

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框架的基本调用原理已经全部梳理完毕,最后,做个总结:

  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的方法,然后回调给它。其实想想,我们自己就可以写一个类似的封装组件。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fastsy

打赏一份隆江猪脚饭吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值