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

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。



转自: https://www.jianshu.com/p/0463c79d533d
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页