美文网首页
Android与H5交互——JSBridge

Android与H5交互——JSBridge

作者: morning_dew | 来源:发表于2020-07-24 17:47 被阅读0次

    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

    相关文章

      网友评论

          本文标题:Android与H5交互——JSBridge

          本文链接:https://www.haomeiwen.com/subject/luxslktx.html