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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容