JsBridge实现及原理

来源:互联网 发布:武汉知黛化妆品靠谱吗 编辑:程序博客网 时间:2024/05/21 14:43

一、Js调用Java,Java调用Js

在Android开发中,能实现Js调用Java,有4种方法:
1.JavascriptInterface
2.WebViewClient.shouldOverrideUrlLoading()
3.WebChromeClient.onConsoleMessage()
4.WebChromeClient.onJsPrompt()


1.1 JavascriptInterface

这是Android提供的Js与Native通信的官方解决方案。
首先Java代码要实现这么一个类,它的作用是提供给Js调用。

public class JavascriptInterface {    @JavascriptInterface    public void showToast(String toast) {        Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();    }}


然后把这个类添加到WebView的JavascriptInterface中。webView.addJavascriptInterface(new JavascriptInterface(), “javascriptInterface”); 在Js代码中就能直接通过“javascriptInterface”直接调用了该Native的类的方法。

function showToast(toast) {    javascript:javascriptInterface.showToast(toast);}


但是这个官方提供的解决方案在Android4.2之前存在严重的安全漏洞。在Android4.2之后,加入了@JavascriptInterface才得到解决。所以考虑到兼容低版本的系统,JavascriptInterface并不适合。


1.2 WebViewClient.shouldOverrideUrlLoading()

这个方法的作用是拦截所有WebView的Url跳转。页面可以构造一个特殊格式的Url跳转,shouldOverrideUrlLoading拦截Url后判断其格式,然后Native就能执行自身的逻辑了。

public class CustomWebViewClient extends WebViewClient {    @Override    public boolean shouldOverrideUrlLoading(WebView view, String url) {        if (isJsBridgeUrl(url)) {            // JSbridge的处理逻辑            return true;        }        return super.shouldOverrideUrlLoading(view, url);    }}


1.3 WebChromeClient.onConsoleMessage()

这是Android提供给Js调试在Native代码里面打印日志信息的API,同时这也成了其中一种Js与Native代码通信的方法。在Js代码中调用console.log(‘xxx’)方法。

console.log('log message that is going to native code')

就会在Native代码的WebChromeClient.consoleMessage()中得到回调。consoleMessage.message()获得的正是Js代码console.log(‘xxx’)的内容。

public class CustomWebChromeClient extends WebChromeClient {    @Override    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {        super.onConsoleMessage(consoleMessage);        String msg = consoleMessage.message();//JavaScript输入的Log内容    }}


1.4 WebChromeClient.onJsPrompt()

其实除了WebChromeClient.onJsPrompt(),还有WebChromeClient.onJsAlert()和WebChromeClient.onJsConfirm()。顾名思义,这三个Js给Native代码的回调接口的作用分别是展示提示信息,展示警告信息和展示确认信息。鉴于,alert和confirm在Js的使用率很高,所以JSBridge的解决方案中都倾向于选用onJsPrompt()。
Js中调用

window.prompt(message, value)

WebChromeClient.onJsPrompt()就会受到回调。onJsPrompt()方法的message参数的值正是Js的方法window.prompt()的message的值。

public class CustomWebChromeClient extends WebChromeClient {    @Override    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {        // 处理JS 的调用逻辑        result.confirm();        return true;    }}


1.5 Java调用Js

前文提到的4种通信方式都是Js通信Native的Java,而反过来,Java通信Js只有一种方式。那就是调用WebView.loadUrl()去执行一个预先定义好的Js方法。

webView.loadUrl(String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative(%s)", data));


二. 通过onJsPrompt()实现JsBridge

1.1 GitHub开源框架地址:https://github.com/pedant/safe-java-js-webview-bridge

1.2 原理与实现:

思路

首先先说思路,有经验的同学可能都知道Android的WebView中有一个WebChromeClient类,这个类其实就是用来监听一些WebView中的事件的,我们发现其中有三个这样的方法。

@Overridepublic boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {    return super.onJsPrompt(view, url, message, defaultValue, result);}@Overridepublic boolean onJsAlert(WebView view, String url, String message, JsResult result) {    return super.onJsAlert(view, url, message, result);}@Overridepublic boolean onJsConfirm(WebView view, String url, String message, JsResult result) {    return super.onJsConfirm(view, url, message, result);}


好了,说到这里我们前期的准备工作也就做好了,其实就是通过重写WebView中WebChromeClient类的onJsPrompt()方法来进行js和java的通信。这三个方法其实就对应于js中的alert(警告框),comfirm(确认框)和prompt(提示框)方法,那这三个方法有什么用呢?前面我们说了JSBridge的作用是提供一种js和java通信的框架,其实我们可以利用这三个方法去完成这样的事。比如我们可以在js脚本中调用alert方法,这样对应的就会走到WebChromeClient类的onJsAlert()方法中,我们就可以拿到其中的信息去解析,并且做java层的事情。那是不是这三个方法随便选一个就可以呢?其实不是的,因为我们知道,在js中,alert和confirm的使用概率还是很高的,特别是alert,所以我们最好不要使用这两个通道,以免出现不必要的问题。

有了实现方案,下面就是一些具体的细节了,大家有没有想过,怎么样才能让java层知道js脚本需要调用的哪一个方法呢?怎么把js脚本的参数传递进来呢?同步异步的方式又该怎么实现呢?下面提供一种我的思路。

首先大家都知道http是什么,其实我们的JSBridge也可以效仿一下http,定义一个自己的协议。比如规定sheme,path等等。下面来看一下一些的具体内容:

hybrid://JSBridge:1538351/method?{“message”:”msg”}

是不是和http协议有一点像,其实我们可以通过js脚本把这段协议文本传递到onPropmt()方法中并且进行解析。比如,sheme是hyrid://开头的就表示是一个hybrid方法,需要进行解析。后面的method表示方法名,message表示传递的参数等等。

有了这样一套协议,我们就可以去进行我们的通信了。

代码

先看一下我们html和js的代码


有前端经验的同学应该能很轻松的看懂这样的代码,对于看不懂的同学我来解释一下,首先看界面。
<!DOCTYPE HTML><html><head>  <meta charset="utf-8">  <script src="file:///android_asset/jsBridge.js" type="text/javascript"></script></head><body><div class="blog-header">  <h3>JSBridge</h3></div><ul class="entry">    <br/>    <li>        toast展示<br/>        <button onclick="JsBridge.call('JSBridge','toast',{'message':'我是气泡','isShowLong':0},function(res){});">toast</button>    </li>    <br/>    <li>        异步任务<br/>        <button onclick="JsBridge.call('JSBridge','plus',{'data':1},function(res){console.log(JSON.stringify(res))});">plus</button>    </li>    <br/>    <br/></ul></body></html>


(function (win, lib) {    var doc = win.document;    var hasOwnProperty = Object.prototype.hasOwnProperty;    var JsBridge = win.JsBridge || (win.JsBridge = {});    var inc = 1;    var LOCAL_PROTOCOL = 'hybrid';    var CB_PROTOCOL = 'cb_hybrid';    var CALLBACK_PREFIX = 'callback_';    //核心功能,对外暴露    var Core = {        call: function (obj, method, params, callback, timeout) {            var sid;            if (typeof callback !== 'function') {                callback = null;            }            sid = Private.getSid();            Private.registerCall(sid, callback);            Private.callMethod(obj, method, params, sid);        },        //native代码处理 成功/失败 后,调用该方法来通知js        onComplete: function (sid, data) {            Private.onComplete(sid, data);        }    };    //私有功能集合    var Private = {        params: {},        chunks: {},        calls: {},        getSid: function () {            return Math.floor(Math.random() * (1 << 50)) + '' + inc++;        },        buildParam: function (obj) {            if (obj && typeof obj === 'object') {                return JSON.stringify(obj);            } else {                return obj || '';            }        },        parseData: function (str) {            var rst;            if (str && typeof str === 'string') {                try {                    rst = JSON.parse(str);                } catch (e) {                    rst = {                        status: {                            code: 1,                            msg: 'PARAM_PARSE_ERROR'                        }                    };                }            } else {                rst = str || {};            }            return rst;        },        //根据sid注册calls的回调函数        registerCall: function (sid, callback) {            if (callback) {                this.calls[CALLBACK_PREFIX + sid] = callback;            }        },        //根据sid删除calls对应的回调函数,并返回call对象        unregisterCall: function (sid) {            var callbackId = CALLBACK_PREFIX + sid;            var call = {};            if (this.calls[callbackId]) {                call.callback = this.calls[callbackId];                delete this.calls[callbackId];            }            return call;        },        //生成URI,调用native功能        callMethod: function (obj, method, params, sid) {            // hybrid://objectName:sid/methodName?params            params = Private.buildParam(params);            var uri = LOCAL_PROTOCOL + '://' + obj + ':' + sid + '/' + method + '?' + params;            var value = CB_PROTOCOL + ':';            window.prompt(uri, value);        },        onComplete: function (sid, data) {            var callObj = this.unregisterCall(sid);            var callback = callObj.callback;            data = this.parseData(data);            callback && callback(data);        }    };    for (var key in Core) {        if (!hasOwnProperty.call(JsBridge, key)) {            JsBridge[key] = Core[key];        }    }})(window);



jsinterface

可以看到有两个按钮,对应着html的这段代码

<br/><li>    toast展示<br/>    <button onclick="JsBridge.call('JSBridge','toast',{'message':'我是气泡','isShowLong':0},function(res){});">toast</button></li><br/><li>    异步任务<br/>    <button onclick="JsBridge.call('JSBridge','plus',{'data':1},function(res){console.log(JSON.stringify(res))});">toast</button></li><br/>

call: function (obj, method, params, callback, timeout) {    var sid;    if (typeof callback !== 'function') {        callback = null;    }    sid = Private.getSid();    Private.registerCall(sid, callback);    Private.callMethod(obj, method, params, sid);}


点击按钮会执行js脚本的这段代码

它其实就是一个函数,名字叫call,括号里的是它的参数(obj, method, params, callback, timeout)。那这几个参数是怎么传递的呢?回过头看我们的html代码,点击第一个按钮,会执行这个语句

<button onclick="JsBridge.call('JSBridge','toast',{'message':'我是气泡','isShowLong':0},function(res){});">toast</button>


然后在call这个方法内,会执行Private类的registerCall和callMethod,我们来看callMehod()。其中括号(‘JSBridge’,’toast’,{‘message’:’我是气泡’,’isShowLong’:0},function(res){})里的第一个参数’JSBridge’对应着前面的obj,’toast’对应着method,以此类推。第二个按钮也是一样。

//生成URI,调用native功能callMethod: function (obj, method, params, sid) {    // hybrid://objectName:sid/methodName?params    params = Private.buildParam(params);    var uri = LOCAL_PROTOCOL + '://' + obj + ':' + sid + '/' + method + '?' + params;    var value = CB_PROTOCOL + ':';    window.prompt(uri, value);}


好了,我们总结一下这两个前端的代码。其实很简单,以界面的第一个按钮toast为例,点击这个按钮,它会执行相应的js脚本代码,然后就会像我们前面所讲的那样,走到onJsPrompt()方法中,下面让我们看看对应的java代码。注释说的很清楚了,就是通过传递进来的参数生成uri,并且调用window.prompt()方法,这个方法大家应该很眼熟吧,没错,在调用这个方法之后,程序就会相应的走到java代码的onJsPrompt()方法中。而生成的uri则是我们上面说过的那个我们自己定义的协议格式。

public class InjectedChromeClient extends WebChromeClient {    private final String TAG = "InjectedChromeClient";    private JsCallJava mJsCallJava;    public InjectedChromeClient() {        mJsCallJava = new JsCallJava();    }    @Override    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {        result.confirm(mJsCallJava.call(view, message));        return true;    }}


jsmessage这是对应的WebChromeClient类,可以看到在onJsPrompt()方法中我们只做了一件事,就是丢给JsCallJava类去解析,再看JsCallJava类之前,我们可以先看看onJsPrompt()这个方法到底传进来了什么。

可以看到,我们传给JsCallJava类的那个message,就像我们前面定义的协议一样。sheme是hybrid://,表示这是一个hybrid方法,host是JSBridge,方法名字是toast,传递的参数是以json格式传递的,具体内容如图。不知道大家有没有发现,这里我有一个东西没有讲,就是JSBridge:后面的那串数字,这串数字是干什么用的呢?大家应该知道,现在我们整个调用过程都是同步的,这意味着我们没有办法在里面做一些异步的操作,为了满足异步的需求,我们就需要定义这样的port,有了这串数字,我们在java层就可以做异步的操作,等操作完成以后回调给js脚本,js脚本就通过这串数字去得到对应的callback,有点像startActivity中的那个requestCode。大家没听懂也没关系,后面我会在代码中具体讲解。

好了,下面我们可以来看JsCallJava这个类的具体代码了。

public class JsCallJava {    private final static String TAG = "JsCallJava";    private static final String BRIDGE_NAME = "JSBridge";    private static final String SCHEME="hybrid";    private static final int RESULT_SUCCESS=200;    private static final int RESULT_FAIL=500;    private ArrayMap<String, ArrayMap<String, Method>> mInjectNameMethods = new ArrayMap<>();    private JSBridge mWDJSBridge = JSBridge.getInstance();    public JsCallJava() {        try {            ArrayMap<String, Class<? extends IInject>> externals = mWDJSBridge.getInjectPair();            if (externals.size() > 0) {                Iterator<String> iterator = externals.keySet().iterator();                while (iterator.hasNext()) {                    String key = iterator.next();                    Class clazz = externals.get(key);                    if (!mInjectNameMethods.containsKey(key)) {                        mInjectNameMethods.put(key, getAllMethod(clazz));                    }                }            }        } catch (Exception e) {            Log.e(TAG, "init js error:" + e.getMessage());        }    }    private ArrayMap<String, Method> getAllMethod(Class injectedCls) throws Exception {        ArrayMap<String, Method> mMethodsMap = new ArrayMap<>();        //获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法        Method[] methods = injectedCls.getDeclaredMethods();        for (Method method : methods) {            String name;            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {                continue;            }           Class[] parameters=method.getParameterTypes();           if(null!=parameters && parameters.length==3){               if(parameters[0]==WebView.class && parameters[1]==JSONObject.class && parameters[2]==JsCallback.class){                   mMethodsMap.put(name, method);               }           }        }        return mMethodsMap;    }    public String call(WebView webView, String jsonStr) {        String methodName = "";        String name = BRIDGE_NAME;        String param = "{}";        String result = "";        String sid="";        if (!TextUtils.isEmpty(jsonStr) && jsonStr.startsWith(SCHEME)) {            Uri uri = Uri.parse(jsonStr);            name = uri.getHost();            param = uri.getQuery();            sid = getPort(jsonStr);            String path = uri.getPath();            if (!TextUtils.isEmpty(path)) {                methodName = path.replace("/", "");            }        }        if (!TextUtils.isEmpty(jsonStr)) {            try {                ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);                Object[] values = new Object[3];                values[0] = webView;                values[1] = new JSONObject(param);                values[2]=new JsCallback(webView,sid);                Method currMethod = null;                if (null != methodMap && !TextUtils.isEmpty(methodName)) {                    currMethod = methodMap.get(methodName);                }                // 方法匹配失败                if (currMethod == null) {                    result = getReturn(jsonStr, RESULT_FAIL, "not found method(" + methodName + ") with valid parameters");                }else{                    result = getReturn(jsonStr, RESULT_SUCCESS, currMethod.invoke(null, values));                }            } catch (Exception e) {                e.printStackTrace();            }        } else {            result = getReturn(jsonStr, RESULT_FAIL, "call data empty");        }        return result;    }    private String getPort(String url) {        if (!TextUtils.isEmpty(url)) {            String[] arrays = url.split(":");            if (null != arrays && arrays.length >= 3) {                String portWithQuery = arrays[2];                arrays = portWithQuery.split("/");                if (null != arrays && arrays.length > 1) {                    return arrays[0];                }            }        }        return null;    }    private String getReturn(String reqJson, int stateCode, Object result) {        String insertRes;        if (result == null) {            insertRes = "null";        } else if (result instanceof String) {            //result = ((String) result).replace("\"", "\\\"");            insertRes = String.valueOf(result);        } else if (!(result instanceof Integer)                && !(result instanceof Long)                && !(result instanceof Boolean)                && !(result instanceof Float)                && !(result instanceof Double)                && !(result instanceof JSONObject)) {    // 非数字或者非字符串的构造对象类型都要序列化后再拼接            insertRes = result.toString();//mGson.toJson(result);        } else {  //数字直接转化            insertRes = String.valueOf(result);        }        //String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes);        Log.d(TAG, " call json: " + reqJson + " result:" + insertRes);        return insertRes;    }}


public String call(WebView webView, String jsonStr) {    String methodName = "";    String name = BRIDGE_NAME;    String param = "{}";    String result = "";    String sid="";    if (!TextUtils.isEmpty(jsonStr) && jsonStr.startsWith(SCHEME)) {        Uri uri = Uri.parse(jsonStr);        name = uri.getHost();        param = uri.getQuery();        sid = getPort(jsonStr);        String path = uri.getPath();        if (!TextUtils.isEmpty(path)) {            methodName = path.replace("/", "");        }    }    if (!TextUtils.isEmpty(jsonStr)) {        try {            ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);            Object[] values = new Object[3];            values[0] = webView;            values[1] = new JSONObject(param);            values[2]=new JsCallback(webView,sid);            Method currMethod = null;            if (null != methodMap && !TextUtils.isEmpty(methodName)) {                currMethod = methodMap.get(methodName);            }            // 方法匹配失败            if (currMethod == null) {                result = getReturn(jsonStr, RESULT_FAIL, "not found method(" + methodName + ") with valid parameters");            }else{                result = getReturn(jsonStr, RESULT_SUCCESS, currMethod.invoke(null, values));            }        } catch (Exception e) {            e.printStackTrace();        }    } else {        result = getReturn(jsonStr, RESULT_FAIL, "call data empty");    }    return result;}


有点长,不过其实逻辑很好理解。首先我们调用的是call这个方法。它里面做了什么呢

可以看到其实就是通过js脚本传递过来的参数得到了方法名字,sid(前面说的那串数字)等等内容。下面看这段代码

1
ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);

通过name去得到一个map,这里的name是我们刚刚解析得到了,对应实际情况就是JSBridge,那这个mInjectNameMethods又是什么呢?

private ArrayMap<String, ArrayMap<String, Method>> mInjectNameMethods = new ArrayMap<>();private JSBridge mJSBridge = JSBridge.getInstance();public JsCallJava() {    try {        ArrayMap<String, Class<? extends IInject>> externals = mJSBridge.getInjectPair();        if (externals.size() > 0) {            Iterator<String> iterator = externals.keySet().iterator();            while (iterator.hasNext()) {                String key = iterator.next();                Class clazz = externals.get(key);                if (!mInjectNameMethods.containsKey(key)) {                    mInjectNameMethods.put(key, getAllMethod(clazz));                }            }        }    } catch (Exception e) {        Log.e(TAG, "init js error:" + e.getMessage());    }}private ArrayMap<String, Method> getAllMethod(Class injectedCls) throws Exception {    ArrayMap<String, Method> mMethodsMap = new ArrayMap<>();    //获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法    Method[] methods = injectedCls.getDeclaredMethods();    for (Method method : methods) {        String name;        if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {            continue;        }       Class[] parameters=method.getParameterTypes();       if(null!=parameters && parameters.length==3){           if(parameters[0]==WebView.class && parameters[1]==JSONObject.class && parameters[2]==JsCallback.class){               mMethodsMap.put(name, method);           }       }    }    return mMethodsMap;}


public class JSBridge {    public static final String BRIDGE_NAME = "JSBridge";    private static JSBridge INSTANCE = new JSBridge();    private boolean isEnable=true;    private ArrayMap<String, Class<? extends IInject>> mClassMap = new ArrayMap<>();    private JSBridge() {        mClassMap.put(BRIDGE_NAME, JSLogical.class);    }    public static JSBridge getInstance() {        return INSTANCE;    }    public boolean addInjectPair(String name, Class<? extends IInject> clazz) {        if (!mClassMap.containsKey(name)) {            mClassMap.put(name, clazz);            return true;        }        return false;    }    public boolean removeInjectPair(String name,Class<? extends IInject> clazz) {        if (TextUtils.equals(name,BRIDGE_NAME)) {            return false;        }        Class clazzValue=mClassMap.get(name);        if(null!=clazzValue && (clazzValue == clazz)){            mClassMap.remove(name);            return true;        }        return false;    }    public ArrayMap<String, Class<? extends IInject>> getInjectPair() {        return mClassMap;    }}


可以看到我们有一个JSBridge类,在JsCallJava的构造函数中,我们通过JSBridge这个类的getInjectPair()方法得到了一个String和class的映射关系,并且把class中符合标准的方法拿出来存放到mInjectNameMethods中,以便我们在call方法中调用。下面来看看JSBridge类。

它的getInjectPair方法其实就是得到了mClassMap,这个map在JSBridge类初始化的时候就有一个默认的值了。

public static final String BRIDGE_NAME = "JSBridge";private JSBridge() {    mClassMap.put(BRIDGE_NAME, JSLogical.class);}


public class JSLogical implements IInject {    /**     * toast     *     * @param webView 浏览器     * @param param   提示信息     */    public static void toast(WebView webView, JSONObject param, final JsCallback callback) {        String message = param.optString("message");        int isShowLong = param.optInt("isShowLong");        Toast.makeText(webView.getContext(), message, isShowLong == 0 ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();        if (null != callback) {            try {                JSONObject object = new JSONObject();                object.put("result", true);                invokeJSCallback(callback, object);            } catch (Exception e) {                e.printStackTrace();            }        }    }    /**     * 加一     *     * @param webView     * @param param     * @param callback     */    public static void plus(WebView webView, final JSONObject param, final JsCallback callback) {        new Thread(new Runnable() {            @Override            public void run() {                try {                    Thread.sleep(2000);                    int original = param.optInt("data");                    original = original + 1;                    if (null != callback) {                        JSONObject object = new JSONObject();                        object.put("after plussing", original);                        invokeJSCallback(callback, object);                    }                } catch (Exception e) {                    e.printStackTrace();                }            }        }).start();    }    private static void invokeJSCallback(JsCallback callback, JSONObject objects) {        invokeJSCallback(callback, true, null, objects);    }    public static void invokeJSCallback(JsCallback callback, boolean isSuccess, String message, JSONObject objects) {        try {            callback.apply(isSuccess, message, objects);        } catch (JsCallback.JsCallbackException e) {            e.printStackTrace();        }    }


key是”JSBridge”,value是我们的JSLogincal类。

对这个类上面的两个方法有没有很眼熟?名字和js脚本中的那两个方法一样有木有。我们调用链最后就会走到相应的同名方法中!

上面就是js调js的整个过程了,其实吧,不应该放这么多的代码的,搞得像是源码分析一样,不过我觉得这样还是有一定好处的,至少跟着代码走一遍能加深印象嘛。

我们还是来捋一捋整个过程。

(1) 在js脚本中把对应的方法名,参数等写成一个符合协议的uri,并且通过window.prompt方法发送给java层。
(2) 在java层的onJsPrompt方法中接受到对应的message之后,通过JsCallJava类进行具体的解析。
(3) 在JsCallJava类中,我们解析得到对应的方法名,参数等信息,并且在map中查找出对应的类的方法。

这里多说一句,还记得我们定义的协议中的host是什么吗?

hybrid://JSBridge:875725/toast?{“message”:”我是气泡”,”isShowLong”:0}

是JSBridge,而我们在JsCallJava类中是通过这个host去查找对应的类的,我们可以看到在JSBridge类中

public static final String BRIDGE_NAME = "JSBridge";private JSBridge() {    mClassMap.put(BRIDGE_NAME, JSLogical.class);}


可能有的同学会说何必这么麻烦,直接在JsCallJava类中定义方法不就好了,这样还省的去写那么多的逻辑。可是大家有想过如果你把所有js脚本想要调用的方法都写在JsCallJava类中,这个类会有多难扩展和维护吗?而像我这样,如果你的js脚本处理的是登录相关逻辑,你可以写一个LoginLogical.class,如果是业务相关,你可以写一个BizLogical.class,这样不仅清晰,而且解耦。这意味着,如果你可以更换你的host,叫aaa都没关系,只要你在对应的map中的key也是aaa就可以了。

当然,如果你仔细的看过代码,会发现其实在native层的那些同名函数其实是有规范的。

首先必须要是public static的,因为这样调用会更方便。

其次参数也有要求,有且仅有三个参数,WebView,JsonObject和一个Callback。WebView用来提供可能需要的context,另外java执行js方法也需要WebView对象。JsonObject是js脚本传递过来的参数。而Callback则是java用于回调js脚本的。

可能你会发现JSBridge里处处都是规范,协议需要规范,参数需要规范。这些其实都是合理的,因为规范所以安全。

#####(4) 在得到对应的方法之后,就去调用它,以我们的toast为

/** * toast * * @param webView 浏览器 * @param param   提示信息 */public static void toast(WebView webView, JSONObject param, final JsCallback callback) {    String message = param.optString("message");    int isShowLong = param.optInt("isShowLong");    Toast.makeText(webView.getContext(), message, isShowLong == 0 ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();    if (null != callback) {        try {            JSONObject object = new JSONObject();            object.put("result", true);            invokeJSCallback(callback, object);        } catch (Exception e) {            e.printStackTrace();        }    }}


以上就是全部js调用java的过程,那我们java执行完逻辑以后,怎么回调js呢?这里我们以另外一个按钮的例子来说。拿到对应的信息,直接makeToast就好了。

<button onclick="JsBridge.call('JSBridge','plus',{'data':1},function(res){console.log(JSON.stringify(res))});">plus</button>


/** * 加一 * * @param webView * @param param * @param callback */public static void plus(WebView webView, final JSONObject param, final JsCallback callback) {    new Thread(new Runnable() {        @Override        public void run() {            try {                Thread.sleep(2000);                int original = param.optInt("data");                original = original + 1;                if (null != callback) {                    JSONObject object = new JSONObject();                    object.put("after plussing", original);                    invokeJSCallback(callback, object);                }            } catch (Exception e) {                e.printStackTrace();            }        }    }).start();}


js脚本传递的一个json的参数,{“data”:1},从名字可以看出是先要java执行一个加逻辑。

这里我们模拟一下耗时操作,可以帮助大家更好的理解JSBridge中的异步操作。对应java层的方法执行完+1的操作之后,把结果封装成一个jsonObject,并且调用invokeJSCallback方法。

public static void invokeJSCallback(JsCallback callback, boolean isSuccess, String message, JSONObject objects) {    try {        callback.apply(isSuccess, message, objects);    } catch (JsCallback.JsCallbackException e) {        e.printStackTrace();    }}

private static final String CALLBACK_JS_FORMAT = "javascript:JsBridge.onComplete('%s', %s);";public void apply(boolean isSuccess, String message, JSONObject object) throws JsCallbackException {    if (mWebViewRef.get() == null) {        throw new JsCallbackException("the WebView related to the JsCallback has been recycled");    }    if (!mCouldGoOn) {        throw new JsCallbackException("the JsCallback isn't permanent,cannot be called more than once");    }    JSONObject result = new JSONObject();    try {        JSONObject code=new JSONObject();        code.put("code", isSuccess ? 0 : 1);        if(!isSuccess && !TextUtils.isEmpty(message)){            code.putOpt("msg",message);        }        if(isSuccess){            code.putOpt("msg", TextUtils.isEmpty(message)?"SUCCESS":message);        }        result.putOpt("status", code);        if(null!=object){            result.putOpt("data",object);        }    } catch (Exception e) {        e.printStackTrace();    }    final String jsFunc = String.format(CALLBACK_JS_FORMAT, mSid, String.valueOf(result));    if (mWebViewRef != null && mWebViewRef.get() != null) {        mHandler.post(new Runnable() {            @Override            public void run() {                mWebViewRef.get().loadUrl(jsFunc);            }        });    }}


invokeJSCallback方法中直接调用了callback的apply方法。

在apply方法中,我们直接拼装了一个jsonObject,里面包括了我们想要返回给js脚本的结果,并且直接调用了js的onComplete方法。

onComplete: function (sid, data) {    var callObj = this.unregisterCall(sid);    var callback = callObj.callback;    data = this.parseData(data);    callback && callback(data);}

function(res){console.log(JSON.stringify(res))}

可以看到js的onComplete通过sid(那一串数字)拿到对应的callback并执行,而我们plus的callback里做了什么呢?

直接在控制台中输出结果。

所以当我们点击plug按钮以后,过两秒我们就可以在logcat中看到如下输出

output

好了,至此所有和JSBridge相关的代码就分析完了。其实原理非常的简单,通过js的window.prompt方法将事先定义好的协议文本传输到java层,然后java层进行解析并调用相应的方法,最后通过callback将结果返回给js脚本。中间我们使用的那些类可以更好的解耦,如果你有心,甚至可以把所用逻辑相关代码抽离出来,把剩余的代码写成JSBridge.core作为库来使用。这样你想加什么功能直接写,不用改任何的源码。

三、UrlRouter(即重写shouldOverrideUrlLoading()方法

3.1 GitHub地址:https://github.com/lzyzsd/JsBridge

3.2 原理与实现:

3.2.1 Java调用Js的functionInJs方法的流程图

3.2.2 JsBridge的UML图

3.2.3 JsBridge工作步骤讲解

3.2.3.1 WebView加载html页面

webView.registerHandler(“submitFromWeb”, …);这是Java层注册了一个叫”submitFromWeb”的接口方法,目的是提供给Js来调用。这个”submitFromWeb”的接口方法的回调就是BridgeHandler.handler()。webView.callHandler(“functionInJs”, …, new CallBackFunction());这是Java层主动调用Js的”functionInJs”方法。

public class MainActivity extends Activity implements OnClickListener {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        webView = (BridgeWebView) findViewById(R.id.webView);        webView.loadUrl("file:///android_asset/demo.html");        webView.registerHandler("submitFromWeb", new BridgeHandler() {            @Override            public void handler(String data, CallBackFunction function) {                Log.i(TAG, "handler = submitFromWeb, data from web = " + data);                function.onCallBack("submitFromWeb exe, response data 中文 from Java");            }        });        webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {            @Override            public void onCallBack(String data) {            }        });    }}


我们一层层深入callHandler()方法的实现。这其中会调用到doSend()方法,这里想解释下m.setCallbackId(callbackStr)方法的作用。该方法设置的callbackId生成后不仅仅会被传到Js,而且会以key-value对的形式和responseCallback配对保存到responseCallbacks这个Map里面。它的目的,就是为了等Js把处理结果回调给Java层后,Java层能根据callbackId找到对应的responseCallback,做后续的回调处理。

private void doSend(String handlerName, String data, CallBackFunction responseCallback) {        Message m = new Message();        if (!TextUtils.isEmpty(data)) {            m.setData(data);        }        if (responseCallback != null) {            String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));            responseCallbacks.put(callbackStr, responseCallback);            m.setCallbackId(callbackStr);        }        if (!TextUtils.isEmpty(handlerName)) {            m.setHandlerName(handlerName);        }        queueMessage(m);    }


最终可以看到是BridgeWebView.dispatchMessage(Message m)方法调用的是this.loadUrl(),调用了_handleMessageFromNative这个Js方法。那这个Js的方法是哪里来的呢?

final static String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";void dispatchMessage(Message m) {        String messageJson = m.toJson();        //escape special characters for json string        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {            this.loadUrl(javascriptCommand);        }    }


3.2.3.2 加载WebViewJavascriptBridge.js

在WebViewClient.onPageFinished()里面的BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs)。正是把保存在assert/WebViewJavascriptBridge.js加载到WebView中。

package com.github.lzyzsd.jsbridge;public class BridgeWebViewClient extends WebViewClient {      @Override    public void onPageFinished(WebView view, String url) {        super.onPageFinished(view, url);        if (BridgeWebView.toLoadJs != null) {            BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);        }        //        if (webView.getStartupMessage() != null) {            for (Message m : webView.getStartupMessage()) {                webView.dispatchMessage(m);            }            webView.setStartupMessage(null);        }    }}


3.2.3.3 分析WebViewJavascriptBridge.js

我们看看WebViewJavascriptBridge.js的代码,就能找到function _handleMessageFromNative()这个Js方法了。_handleMessageFromNative()方法里面会调用_dispatchMessageFromNative()方法。当处理来自Java层的主动调用时候会走“直接发送”的else分支。message.callbackId会被取出来,实例化一个responseCallback,而它是用来Js处理完成后把结果数据回调给Java层代码的。接着会根据message.handleName(在这个分析例子中,handleName的值就是”functionInJs”)在messageHandlers这个Map去获取handler,最后交给handler去处理。

function _dispatchMessageFromNative(messageJSON) {    setTimeout(function() {        var message = JSON.parse(messageJSON);        var responseCallback;        //java call finished, now need to call js callback function        if (message.responseId) {            ...        } else {            //直接发送            if (message.callbackId) {                var callbackResponseId = message.callbackId;                responseCallback = function(responseData) {                    _doSend({                        responseId: callbackResponseId,                        responseData: responseData                    });                };            }            var handler = WebViewJavascriptBridge._messageHandler;            if (message.handlerName) {                handler = messageHandlers[message.handlerName];            }            //查找指定handler            try {                handler(message.data, responseCallback);            } catch (exception) {                if (typeof console != 'undefined') {                    console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);                }            }        }    });}


3.2.3.4 页面Html注册”functionInJs”方法

延续上面的分析,messageHandler是哪里设置的呢。答案就在当初webView.loadUrl(“file:///android_asset/demo.html”);加载的这个demo.html中。bridge.registerHandler(“functionInJs”, …)这里注册了”functionInJs”。

<html>    <head>    ...    </head>    <body>    ...    </body>    <script>        ...        connectWebViewJavascriptBridge(function(bridge) {            bridge.init(function(message, responseCallback) {                console.log('JS got a message', message);                var data = {                    'Javascript Responds': '测试中文!'                };                console.log('JS responding with', data);                responseCallback(data);            });            bridge.registerHandler("functionInJs", function(data, responseCallback) {                document.getElementById("show").innerHTML = ("data from Java: = " + data);                var responseData = "Javascript Says Right back aka!";                responseCallback(responseData);            });        })    </script></html>


3.2.3.5 “functionInJs”执行结果回传Java

“funciontInJs”执行完毕后调用的responseCallback正是_dispatchMessageFromNative()实例化的,而它实际会调用_doSend()方法。_doSend()方法会先把Message推送到sendMessageQueue中。然后修改messagingIframe.src,这里会出发Java层的WebViewClient.shouldOverrideUrlLoading()的回调。

function _doSend(message, responseCallback) {    if (responseCallback) {        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();        responseCallbacks[callbackId] = responseCallback;        message.callbackId = callbackId;    }    sendMessageQueue.push(message);    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;}    在BridgeWebViewClient.shouldOverrideUrlLoading()里面,会先执行webView.flushMessageQueue()的分支。@Overridepublic boolean shouldOverrideUrlLoading(WebView view, String url) {    try {        url = URLDecoder.decode(url, "UTF-8");    } catch (UnsupportedEncodingException e) {        e.printStackTrace();    }    if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据        webView.handlerReturnData(url);        return true;    } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //        webView.flushMessageQueue();        return true;    } else {        return super.shouldOverrideUrlLoading(view, url);    }}


webView.flushMessageQueue()首先去执行Js的_flushQueue()方法,并附带着CallBackFunction。Js的_flushQueue()方法会把sendMessageQueue中的所有message都回传给Java层。CallBackFunction就是把messageQueue解析出来后一个一个Message在for循环中处理,也正是在for循环中,”functionInJs”的Java层回调方法被执行了。

void flushMessageQueue() {    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {        loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {            @Override            public void onCallBack(String data) {                // deserializeMessage                List<Message> list = null;                try {                    list = Message.toArrayList(data);                } catch (Exception e) {                    e.printStackTrace();                    return;                }                if (list == null || list.size() == 0) {                    return;                }                for (int i = 0; i < list.size(); i++) {                    ...                }            }        });    }}


到此,JsBridge的调用流程就分析完毕了。虽然JsBridge使用了MessageQueue后,分析起来有点绕。但原理是不变的,Js调用Java是通过WebViewClient.shouldOverrideUrlLoading()。当然,还有在文章开头介绍另外3种方式。Java调用Js是通过WebView.loadUrl(“javascript:xxxx”)。



PS:非本人研究,本人只是搬运工,参考的是以下链接。

JsBridge 实现 JavaScript 和 Java 的互相调用

好好和h5沟通!几种常见的hybrid通信方式

Hybrid 开发:JsBridge - Web和客户端的桥

Android JSBridge的原理与实现

JSBridge深度剖析







1 0