美文网首页JavaScriptandroidJS-Native-JsBridge
JSbridge系列解析(三):lzyzsd/JsBridge源

JSbridge系列解析(三):lzyzsd/JsBridge源

作者: zizi192 | 来源:发表于2017-07-19 09:48 被阅读470次

    JSBrige系列直通车,由浅入深理解JS-Native的通信过程:
    JSbridge系列解析(一):JS-Native调用方法
    JSbridge系列解析(二):lzyzsd/JsBridge使用方法
    JSbridge系列解析(三):lzyzsd/JsBridge源码解析
    JSbridge系列解析(四):Web端发消息给Native代码流程具体分析

    先说结论吧:必须在主线程中调用BridgeWebView的CallHandler方法,否则可能无效。

    开发中遇到一个场景,需要在上传图片过程中,调用CallHandler后,向JS代码传递上传进度。但实际测试发现,JS未触发。下面我们看下JsBridge的实现源码来分析该问题的原因。

    Java代码调用JS

    调用流程引用简书中某作者图,具体可见http://www.jianshu.com/p/fce3e2f9cabc

    调用时序图.png

    以Demo中MainActivity中WebView.CallHandler为例,调用流程比较简单直接,不做过多讲解。直接看源码,如下:

    /**
    * call javascript registered handler
    *
    * @param handlerName  方法名
    * @param data   入参,一般为和服务器约定的gson对象
    * @param callBack  回调函数
    */
    public void callHandler(String handlerName, String data, CallBackFunction callBack) {
        doSend(handlerName, data, callBack);
    }
    
    //1)组装Message对象;2)强回调函数保存在responseCallbacks中
    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);
    }
    
    //将消息加入队列。若队列为空,则直接分发运行
    private void queueMessage(Message m) {
        if (startupMessage != null) {
            startupMessage.add(m);
        } else {
            dispatchMessage(m);
        }
    }
    
    //1)将Message对象转为JS语句,2)通过loadUrl执行JS代码,触发lib库中的_handleMessageFromNative方法
    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);
        }
    }
    

    接下来我们着重看下queueMessage方法,心里会有这样的疑问:startupMessage对象在什么情况下会为空呢。看BridgeWebview的成员变量声明语句,startupMessage在声明时已被初始化。

    //BridgeWebView.java
    private List<Message> startupMessage = new ArrayList<Message>();
    

    在BridgeWebView中,startupMessage并未被赋值为空;这就只能全局搜索startupMessage的引用了。最终在BridgeWebViewClient的onPageFinished方法中找到。

    //BridgeWebViewClient.java
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        //加载asset目录下的WebViewJavascriptBridge.js文件
        if (BridgeWebView.toLoadJs != null) {
            BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
        }
    
        //将startupMessage队列中的所有消息执行,并将队列置为空
        if (webView.getStartupMessage() != null) {
            for (Message m : webView.getStartupMessage()) {
                webView.dispatchMessage(m);
            }
            webView.setStartupMessage(null);
        }
    }
    

    根据我的理解,startupMessage队列主要是用来在JsBridge的js库注入之前,保存Java调用JS的消息,避免消息的丢失或失效。待页面加载完成后,后续CallHandler的调用,可直接使用loadUrl方法而不需入队。究其根本,是因为Js代码库必须在onPageFinished(页面加载完成)中才能注入导致的。

    再回到文章头部的问题,开发中页面加载完成,此时startupMessage队列为空。用户选择图片上传时,由于上传是异步线程,进度回调也运行在非ui线程。当调用BridgeWebView的dispatchMessage方法时,因当前线程为非主线程,导致并未触发loadUrl。解决方法时必须在主线程中调用CallHandler方法

    WebViewJavascriptBridge.js实现

    接下来看JsBridge库中WebViewJavascriptBridge.js代码,CallHandler方法最终会执行js中_handleMessageFromNative方法

    //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以最终调用了_dispatchMessageFromNative方法
    function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue && receiveMessageQueue.length > 0) {
            receiveMessageQueue.push(messageJSON);
        } else {
             _dispatchMessageFromNative(messageJSON);
        }
    }
    
    //提供给native使用,
    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) {
                    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
                            });
                        };
                    }
    
                    //获取默认handler。若message设置了handlerName,则在messageHandlers中依据名字获取
                    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);
                        }
                    }
                }
            });
        }
    
        //存储注册的Handler(assigned handlerName)
        function registerHandler(handlerName, handler) {
            messageHandlers[handlerName] = handler;
        }
    

    根据CallHandler调用过程中Message的创建代码,其responseId为null,故最终调用handler = messageHandlers[message.handlerName]。该队列中存储Js注册给Java调用的Handler方法,即源码示例的demo.html文件中的functionInJs。

    //demo.html
    connectWebViewJavascriptBridge(function(bridge) {
                //初始化,设置WebViewJavascriptBridge._messageHandler
                bridge.init(function(message, responseCallback) {
                    console.log('JS got a message', message);
                    var data = {
                        'Javascript Responds': '测试中文!'
                    };
                    console.log('JS responding with', data);
                    responseCallback(data);
                });
    
                //注册方法供java调用
                bridge.registerHandler("functionInJs", function(data, responseCallback) {
                    document.getElementById("show").innerHTML = ("data from Java: = " + data);
                    var responseData = "Javascript Says Right back aka!";
                    responseCallback(responseData);
                });
    })
    

    JS代码调用Java

    CallBack调用时序.png

    实现原理:利用js的iFrame(不显示)的src动态变化,触发java层WebViewClient的shouldOverrideUrlLoading方法,然后让本地去调用javasript。
    JS代码执行完成后,最终调用_doSend方法处理回调。

        //sendMessage add message, 触发native处理 sendMessage.【JS代码】
        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;
        }
    

    iFrame变更后,java部分触发shouldOverrideUrlLoading方法,根据scheme不同,进入webview的flushMessageQueue方法。该方法最终调用JS的_fetchQueue方法。

    // 提供给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
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
    }
    

    上述方法运行后,iFrame再次变更,java部分触发shouldOverrideUrlLoading方法,根据scheme不同,进入webview的handlerReturnData方法,实现java回调函数的调用。

    疑问:目前还未想明白_doSend为什么不直接调用_fetchQueue,而必须通过Java代码转一圈。后续明白了再补充吧。

    目前想到的使用_fetchQueue的一个优点,可以批量处理Message。因_doSend中将待处理的message放入sendMessageQueue,而_fetchQueue中将队列中消息全部取出转为json数据传递给WebViewClient。

    相关文章

      网友评论

        本文标题:JSbridge系列解析(三):lzyzsd/JsBridge源

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