JsBrigde源码分析

在 Android 中 WebView 是与 JS 交互的一个桥梁, JsBrigde 的本质也是通过 WebView 的方法调用的 JS ,JS 同样可以调 WebView。

对于 Android 调用 JS 码的方法有 2 种:

1.通过 WebView 的 loadUrl()
2.通过 WebView 的 evaluateJavascript()

对于 JS 调用 Android 代码的方法有 3 种:

1.通过 WebView 的 addJavascriptInterface() 进行对象映射
2.通过 WebViewClient 的 shouldOverrideUrlLoading () 方法回调拦截 url
3.通过 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截JS对话框 alert()、confirm()、prompt() 消息

在 JsBrigde 库中 Native 的部分就是通过 webView.loadUrl() 来调用的 JS 代码,而 JS 的部分则是通过刷新 ifream.src 属性来通知 WebViewClient 的 shouldOverrideUrlLoading ()方法,把数据传递到 Native 的。双方使用 json 格式来传递数据。

JsBrigde源码分析

无论是 JS 调 Native,还是 Native 调 JS,两边要约定好 handlerName,调用方传递的 handlerName,接收方同样需要一致的 handlerName,来保证双方能够正常通信。

window.WebViewJavascriptBridge.callHandler(
        'submitFromWeb'
        , {'param': '中文测试'}
        , function(responseData) {
            document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
        }
    );

webView.registerHandler("submitFromWeb", new BridgeHandler() {

    @Override
    public void handler(String data, CallBackFunction function) {
        Log.e(TAG, "handler = submitFromWeb, data from web = " + data);
        function.onCallBack("submitFromWeb exe, response data 中文 from Java");
    }

});

Js -> Native -> Js

下面这张图就是 Js -> Native -> Js 的一个过程,可以结合这张图,来看下面的分析


js_native_js.png

先以 JS 调 Native 为例,在 H5 中想调 Native 方法 ,执行 WebViewJavascriptBridge.callHandler 方法,此方法有三个参数,分别是: 与 Native 约定好的 handlerName,要发送的数据data,及用来接收 Native 返回数据的 responseCallBack,在 callHandler 方法中就干了一件事调用 _doSend 方法

//JS call native method

window.WebViewJavascriptBridge.callHandler(
        'submitFromWeb'
        , {'param': '中文测试'}
        , function(responseData) {
            document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
        }
    );

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

在 _doSend 方法中,首先判断了有没有 responseCallback,如果有,创建一个callbackId 赋值于 Message 对象,创建 responseCallBack 数组存储 responseCallback,创建 MessageQueue 添加 Message,然后调用 Iframe.src 属性,刷新这个属性时,会走到 Native 中的 BridgeWebViewClient.shouldOverrideUrlLoading 方法,此方法可以拦截 URL

//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;
}

在 BridgeWebViewClient 的 shouldOverrideUrlLoading 方法中可以拿到 JS 中 ifream.src 刷新后的 URL,根据URL 的前缀调用了 webView.flushMessageQueue();

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

    if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
        webView.handlerReturnData(url);
        return true;
    } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
        webView.flushMessageQueue();
        return true;
    } else {
        return super.shouldOverrideUrlLoading(view, url);
    }
}

在 flushMessageQueue 方法中先不看 CallBackFunction 的实现,在这里调用了 webView 的 loadUrl() 方法来调 JS 的 _fetchQueue();

  //Js 方法
String JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();"

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

public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
    this.loadUrl(jsUrl);
    // 添加至 Map<String, CallBackFunction>
    responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}

在这里再次调用了Iframe.src,把 _doSend 方法存储的 Message 数组转换成 json 字符串,然后拼接成了 URL 回传到 Native,这时再次走到 BridgeWebViewClient.shouldOverrideUrlLoading 方法

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

这次根据URL前缀会调用 handlerReturnData(url)方法,并把URL数据传过去。

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

    if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
        webView.handlerReturnData(url);
        return true;
    } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
        webView.flushMessageQueue();
        return true;
    } else {
        return super.shouldOverrideUrlLoading(view, url);
    }
}

在这里解析了URL,取出 functionName,CallBackFunction 及 data,并把 data 塞给 CallBackFunction。

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;
    }
}

现在来看 CallBackFunction 的实现,这代码有点长,不过这次调用只会走一部分代码,首先把 data 转成了 list,并且遍历这个 list 取出 Message 及 Message 中的 responseId,这个时候 responseId 应该是不存在的,因为这时候 js 传递过来的数据就没有 responseId,可以回到 _doSend 方法看一下,Message 对象只存储了callbackId,所以会走到 else 里,在这里取 callbackId 判断是否为空,如果不为空,说明 JS 在调用 Native 方法后还需要 Native 这边的 “反馈”,在 callHandler 是就说了第三个参数 responseCallBack 是用来接受 Native 返回来数据的回调, 那么这时候实现了 Native 外部 registerHandler 该方法的 CallBackFunction 接口,把 data 数据封装成 Message 对象,给 Message 对象设置了一个 responseId, 接着调用了 queueMessage(responseMsg); 如果为空就说明 JS 不需要 Native 的数据回调,而CallBackFunction也是空实现,最终根据 handlerName 取出来 Native 对应注册的方法,把数据传给上层,这样上层的registerHandler方法就收到 JS 传过来的 data 数据了。

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 如果有回调Id
                        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);
                        }
                    }
                }
            }
        });

还没完,JS 还没收到 Native 的数据回调,接着看 queueMessage(responseMsg) , 直接调了 dispatchMessage,在 dispatchMessage 中,把 Message 转换 Json 字符串,拼接了 JS指令 ,通过 webview.loadUrl() 调用了 JS 方法 _handleMessageFromNative

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

String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";

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);
    }
}

_handleMessageFromNative 调用了 _dispatchMessageFromNative 方法,同样这段代码也很长,仔细看的话,这段 JS 代码和 Native 的 CallBackFunction 里的代码逻辑上很像,这里依然只会走一部分代码,在这里也会先取Message对象里的 responseId, 很明显Native 传过来的 Message 对象中有 responseId,所以在这里就回调给了上层,callHandler 中的 responseCallBack 回调就有值了。下面的 else 也不会执行。

  //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
function _handleMessageFromNative(messageJSON) {
    console.log(messageJSON);
    if (receiveMessageQueue) {
        receiveMessageQueue.push(messageJSON);
    }
    _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
                    });
                };
            }

            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);
                }
            }
        }
    });
}

到此 JS 调用 Native 方法并且得到 Native 返回的数据回调,整个流程就走了一遍,反过来其实也是一样的,这里就不再跟进了,可以参考下面这张图自己屡一下。刚才说的两段比较长的代码中,其实两边调用都走了这两段代码,每次单项调用就会走方法中对应的某一部分。


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

推荐阅读更多精彩内容