Android与H5交互——JSBridge

2020-07-24  本文已影响0人  morning_dew

1 JSBridge介绍

工程地址:https://github.com/lzyzsd/JsBridge
一句话介绍:This project make a bridge between Java and JavaScript.It provides safe and convenient way to call Java code from js and call js code from java.

2 原理

本文主要阐述Android(Java)与H5(JS)交互,以下描述统一用各自语言替代。

2.1 JS调用Java方法

前端页面通过某种方式触发scheme(如用iframe.src),然后Native用某种方法捕获对应的url触发事件,然后拿到当前的触发url,根据定义好的协议,分析当前触发了那种方法,然后根据定义来执行。
JS方法:

    //sendMessage add message, 触发native处理 sendMessage
    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;
    }

Java回调:

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        String theUrl = url;
        try {
            theUrl = URLDecoder.decode(url, "UTF-8");
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (theUrl.startsWith(BridgeUtil.YY_RETURN_DATA)) {
            webView.handlerReturnData(theUrl);
            return true;
        } else if (theUrl.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
            webView.flushMessageQueue();
            return true;
        } else {
            return super.shouldOverrideUrlLoading(view, theUrl);
        }
    }

2.2 Java调用JS方法

若调用的js方法没有返回值,则直接可以调用mWebView.loadUrl("javascript:do()");其中func是js中的方法;若有返回值时可以调用mWebView.evaluateJavascript("javascript:sum()",new ValueCallback<String>() {})方法。

//js方法没有返回值,JSBridge目前采用的方式
mWebView.loadUrl("javascript:func()");
//js方法有返回值
mWebView.evaluateJavascript("sum(1,2)", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //todo
        }
    }); 

对应的Js代码如下:

<script type="text/javascript">
    function sum(a,b){
        return a+b;
    }
    function do(){
        document.getElementById("p").innerHTML="hello world";
    }
</script>

3 初始化

Android工程需要引入WebViewJavascriptBridge.js文件,并在BridgeWebViewClient调用onPageFinished方法时进行load:

    public static void webViewLoadLocalJs(WebView view, String path){
        String jsContent = assetFile2Str(view.getContext(), path);
        view.loadUrl("javascript:" + jsContent);
    }

WebViewJavascriptBridge.js会执行以下操作来初始化bridge:

    var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
        init: init,
        send: send,
        registerHandler: registerHandler,
        callHandler: callHandler,
        _fetchQueue: _fetchQueue,
        _handleMessageFromNative: _handleMessageFromNative
    };
    var doc = document;
    _createQueueReadyIframe(doc);
    _createQueueReadyIframe4biz(doc);
    var readyEvent = doc.createEvent('Events');
    readyEvent.initEvent('WebViewJavascriptBridgeReady');
    readyEvent.bridge = WebViewJavascriptBridge;
    doc.dispatchEvent(readyEvent);

而H5在使用JSBridge时需要先判断WebViewJavascriptBridge是否存在,如果不存在可以监听WebViewJavascriptBridgeReady event:

    if (window.WebViewJavascriptBridge) {
        //do your work here
    } else {
        document.addEventListener(
            'WebViewJavascriptBridgeReady'
            , function() {
                //do your work here
            },
            false
        );
    }

4 交互流程

下面以takePhoto方法为例,通过源码来分析Js调用Java方法,并获取返回数据的流程。

4.1 JS通知Java有消息来了

H5页面点击拍照,JS调用takePhoto方法:

    takePhoto(obj) {
        return new Promise(function (resolve, reject) {
            window.bridge.callHandler('takePhoto', obj, function (res) {
                if (typeof res == 'string') {
                    res = JSON.parse(res)
                }
                if (res) {
                    resolve(res)
                } else {
                    reject('Get takePhoto info error.')
                }
            })
        })
    },

进入WebViewJavascriptBridge的callHandler及_doSend方法:

    function callHandler(handlerName, data, responseCallback) {
        _doSend({
            handlerName: handlerName,
            data: data
        }, responseCallback);
    }

    //sendMessage add message, 触发native处理 sendMessage
    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;
    }

responseCallback即为JS传入的回调函数(接收原生拍照返回数据),显然不为空。于是生成唯一的callbackId,然后以callbackId为key, 回调函数为value,放在responseCallbacks这个对象里(返回数据时从responseCallbacks找到对应的responseCallback),同时在message这个对象里也存一份。然后再往sendMessageQueue这个数组里push一个message对象。接着会变更messagingIframe元素的的src属性,使得原生回调shouldOverrideUrlLoading。

到这里,JS通知处理完成,Java开始做对应处理。

4.2 Java收到通知,并告诉JS发送消息

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        String theUrl = url;
        try {
            theUrl = URLDecoder.decode(url, "UTF-8");
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (theUrl.startsWith(BridgeUtil.YY_RETURN_DATA)) {
            webView.handlerReturnData(theUrl);
            return true;
        } else if (theUrl.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
            webView.flushMessageQueue();
            return true;
        } else {
            return super.shouldOverrideUrlLoading(view, theUrl);
        }
    }

这里会走第二个if,调用BridgeWebView的flushMessageQueue()方法:

    void flushMessageQueue() {
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

                @Override
                public void onCallBack(String data) {
                    //代码太长,此处省略,后面会展示
                }
            });
        }
    }

    public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
        this.loadUrl(jsUrl);
        responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
    }

在这个方法里,首先会调用WebViewJavascriptBridge的_fetchQueue()方法,然后解析方法名字,即为_fetchQueue。然后以_fetchQueue为key,新创建的回调方法CallBackFunction为value, 放到responseCallbacks(Java中是Map,注意与JS同名的responseCallbacks区分)里面。

到这里,Java响应了JS的QUEUE_HAS_MESSAGE请求,并通过loadUrl的方式请求JS的_fetchQueue()方法。

4.3 JS响应Java调用,并发送请求数据给Java

    // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        //android can't read directly the return data, so we can reload iframe src to communicate with java
        if (messageQueueString !== '[]') {
            bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
        }
    }

sendMessageQueue这个数组我们在_doSend()方法中用到过,里面push了一个message对象,json格式化之后字符串就是[{"handlerName":"takePhoto","data":xxxx,"callbackId":"cb_1_1595831484000"}]这样的,然后将sendMessageQueue这个数组置空, 接着再次变更iframe的src属性,触发java的shouldOverrideUrlLoading方法。

到这里,JS响应了Java的_fetchQueue调用,并再次触发Java的shouldOverrideUrlLoading方法(携带数据)。

4.4 Java收到通知,执行原生逻辑,并发送响应数据给JS

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        String theUrl = url;
        try {
            theUrl = URLDecoder.decode(url, "UTF-8");
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (theUrl.startsWith(BridgeUtil.YY_RETURN_DATA)) {
            webView.handlerReturnData(theUrl);
            return true;
        } else if (theUrl.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
            webView.flushMessageQueue();
            return true;
        } else {
            return super.shouldOverrideUrlLoading(view, theUrl);
        }
    }

首先将url解码,这里会走第一个if,调用handlerReturnData(theUrl)方法:

    void handlerReturnData(String url) {
        String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
        CallBackFunction f = responseCallbacks.get(functionName);
        String data = BridgeUtil.getDataFromReturnUrl(url);
        if (f != null) {
            f.onCallBack(data);
            responseCallbacks.remove(functionName);
            return;
        }
    }

functionName就是_fetchQueue,根据方法名从responseCallbacks这个Map中取出回调方法, 然后解析出data并调用回调方法,最后将这个回调方法从Map中移除。
接着我们看onCallBack中的逻辑(补上flushMessageQueue函数未展示部分):

    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++) {
                        Message m = list.get(i);
                        String responseId = m.getResponseId();
                        // 是否是response  CallBackFunction
                        if (!TextUtils.isEmpty(responseId)) {
                            CallBackFunction function = responseCallbacks.get(responseId);
                            String responseData = m.getResponseData();
                            function.onCallBack(responseData);
                            responseCallbacks.remove(responseId);
                        } else {
                            CallBackFunction responseFunction = null;
                            // if had callbackId
                            final String callbackId = m.getCallbackId();
                            if (!TextUtils.isEmpty(callbackId)) {
                                responseFunction = new CallBackFunction() {
                                    @Override
                                    public void onCallBack(String data) {
                                        Message responseMsg = new Message();
                                        responseMsg.setResponseId(callbackId);
                                        responseMsg.setResponseData(data);
                                        queueMessage(responseMsg);
                                    }
                                };
                            } else {
                                responseFunction = new CallBackFunction() {
                                    @Override
                                    public void onCallBack(String data) {
                                        // do nothing
                                    }
                                };
                            }
                            // BridgeHandler执行
                            BridgeHandler handler;
                            if (!TextUtils.isEmpty(m.getHandlerName())) {
                                handler = messageHandlers.get(m.getHandlerName());
                            } else {
                                handler = defaultHandler;
                            }
                            if (handler != null){
                                handler.handler(m.getData(), responseFunction);
                            }
                        }
                    }
                }
            });
        }
    }

首先将数据解析成一个Message的list,数据类似[{"handlerName":"takePhoto","data":xxxx,"callbackId":"cb_1_1595831484000"}]。
接着遍历这个list中的每一个Message,Message类定义如下:

public class Message {
    private String callbackId; //callbackId
    private String responseId; //responseId
    private String responseData; //responseData
    private String data; //data of message
    private String handlerName; //name of handler
    ...
}

这里只有callbackId,没有responseId,因此会走else分支的里面的if分支,即新建一个CallBackFunction,然后根据Message中的handlerName,从messageHandlers中获取一个BridgeHandler对象,而这个对象需要在Java中注册:

webView.registerHandler("takePhoto", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
               ...
               function.onCallBack(str);
           }
});

public void registerHandler(String handlerName, BridgeHandler handler) {
        if (handler != null) {
            messageHandlers.put(handlerName, handler);
        }
}

因此通过messageHandlers.get(m.getHandlerName())拿到的就是registerHandler时的BridgeHandler,进一步执行handler方法,而这里的function就是前面新建的CallBackFunction。继续执行onCallBack:

 responseFunction = new CallBackFunction() {
        @Override
         public void onCallBack(String data) {
              Message responseMsg = new Message();
              responseMsg.setResponseId(callbackId);
              responseMsg.setResponseData(data);
              queueMessage(responseMsg);
        }
 };

new一个responseMsg,并设置了responseId和responseData,然后调用了queueMessage()方法,进一步执行dispatchMessage:

    private void queueMessage(Message m) {
        if (startupMessage != null) {
            startupMessage.add(m);
        } else {
            dispatchMessage(m);
        }
    }

    void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string  为json字符串转义特殊字符
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'");
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
    }

这里再次通过loadUrl的方式,调用WebViewJavascriptBridge.js的_handleMessageFromNative方法。

4.5 JS响应Java调用,执行回调

    function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue && receiveMessageQueue.length > 0) {
            receiveMessageQueue.push(messageJSON);
        } else {
            _dispatchMessageFromNative(messageJSON);
        }
    }

    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            // {"responseData":"http:\/\/ww3.sinaimg.cn\/mw690\/96a29af5jw8fdfu43tnvlj20ro0rotab.jpg","responseId":"cb_1_1532864396479"}
            var message = JSON.parse(messageJSON);
            var responseCallback;
            // java call finished, now need to call js callback function
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[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);
                    }
                }
            }
        });
    }

这里的messageJSON类似{"responseData":xxxx,"responseId":"cb_1_1595831484000"}, responseData和responseId是创建message时设置的。根据responseId从responseCallbacks(这是JS中的数组,别搞混了)中取出responseCallback,这个responseCallback是在_doSend()方法中存进去的,也就是一开始js中callHandler时传进去的一个方法。将message.responseData传递给这个方法,执行完之后从responseCallbacks这个对象里删除responseCallback方法。至此,JS最终执行了自己的回调function:

takePhoto(obj) {
        return new Promise(function (resolve, reject) {
            window.bridge.callHandler('takePhoto', obj, function (res) {
                if (typeof res == 'string') {
                    res = JSON.parse(res)
                }
                if (res) {
                    resolve(res)
                } else {
                    reject('Get takePhoto info error.')
                }
            })
        })
    },

4.6 流程总结

整个过程主要分为5步,总结起来就是JS做完Java做,Java做完JS做...,而两者通过loadurl和shouldOverrideUrlLoading的方式进行交互,附一张网上的图:


JSBridge

5 存在的问题

BridgeWebView调用dispatchMessage方法时(交互流程4.4),通过loadUrl的方式将数据传递给Js,而loadUrl会自动进行一次urldecode,导致传递给Js的数据与实际不一致,进而出现一些诡异的bug。

  void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string  为json字符串转义特殊字符
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'");
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        // 必须要找主线程才会将数据传递出去 --- 划重点
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
    }

解决方案:android4.4以及以上采用evaluateJavascript() ,android4.4以下针对不同问题需要特殊处理。

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        this.evaluateJavascript(javascriptCommand,null);
    } else  {
        this.loadUrl(javascriptCommand);
    }

6 参考文章

本文部分内容参考以下文章:
Android和H5交互-基础篇 https://www.jianshu.com/p/a25907862523
JsBridge源码分析 https://www.jianshu.com/p/c09667ec4c84

上一篇下一篇

猜你喜欢

热点阅读