JsBrigde源碼分析

在 Android 中 WebView 是與 JS 交互的一個橋梁, JsBrigde 的本質(zhì)也是通過 WebView 的方法調(diào)用的 JS ,JS 同樣可以調(diào) WebView蜀撑。

對于 Android 調(diào)用 JS 碼的方法有 2 種:

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

對于 JS 調(diào)用 Android 代碼的方法有 3 種:

1.通過 WebView 的 addJavascriptInterface() 進行對象映射
2.通過 WebViewClient 的 shouldOverrideUrlLoading () 方法回調(diào)攔截 url
3.通過 WebChromeClient 的 onJsAlert()必盖、onJsConfirm()鄙皇、onJsPrompt() 方法回調(diào)攔截JS對話框 alert()、confirm()、prompt() 消息

在 JsBrigde 庫中 Native 的部分就是通過 webView.loadUrl() 來調(diào)用的 JS 代碼钙姊,而 JS 的部分則是通過刷新 ifream.src 屬性來通知 WebViewClient 的 shouldOverrideUrlLoading ()方法,把數(shù)據(jù)傳遞到 Native 的须妻。雙方使用 json 格式來傳遞數(shù)據(jù)仔蝌。

JsBrigde源碼分析

無論是 JS 調(diào) Native,還是 Native 調(diào) JS荒吏,兩邊要約定好 handlerName敛惊,調(diào)用方傳遞的 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 的一個過程,可以結(jié)合這張圖儡湾,來看下面的分析


js_native_js.png

先以 JS 調(diào) Native 為例特恬,在 H5 中想調(diào) Native 方法 ,執(zhí)行 WebViewJavascriptBridge.callHandler 方法,此方法有三個參數(shù)徐钠,分別是: 與 Native 約定好的 handlerName癌刽,要發(fā)送的數(shù)據(jù)data,及用來接收 Native 返回數(shù)據(jù)的 responseCallBack,在 callHandler 方法中就干了一件事調(diào)用 _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妒穴,如果有,創(chuàng)建一個callbackId 賦值于 Message 對象摊崭,創(chuàng)建 responseCallBack 數(shù)組存儲 responseCallback讼油,創(chuàng)建 MessageQueue 添加 Message,然后調(diào)用 Iframe.src 屬性呢簸,刷新這個屬性時矮台,會走到 Native 中的 BridgeWebViewClient.shouldOverrideUrlLoading 方法,此方法可以攔截 URL

//sendMessage add message, 觸發(fā)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根时,根據(jù)URL 的前綴調(diào)用了 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)) { // 如果是返回數(shù)據(jù)
        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 的實現(xiàn)瘦赫,在這里調(diào)用了 webView 的 loadUrl() 方法來調(diào) 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);
}

在這里再次調(diào)用了Iframe.src,把 _doSend 方法存儲的 Message 數(shù)組轉(zhuǎn)換成 json 字符串蛤迎,然后拼接成了 URL 回傳到 Native确虱,這時再次走到 BridgeWebViewClient.shouldOverrideUrlLoading 方法

  // 提供給native調(diào)用,該函數(shù)作用:獲取sendMessageQueue返回給native,由于android不能直接獲取返回的內(nèi)容,所以使用url shouldOverrideUrlLoading 的方式返回內(nèi)容
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);
}

這次根據(jù)URL前綴會調(diào)用 handlerReturnData(url)方法,并把URL數(shù)據(jù)傳過去替裆。

@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)) { // 如果是返回數(shù)據(jù)
        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;
    }
}

現(xiàn)在來看 CallBackFunction 的實現(xiàn),這代碼有點長把鉴,不過這次調(diào)用只會走一部分代碼故黑,首先把 data 轉(zhuǎn)成了 list,并且遍歷這個 list 取出 Message 及 Message 中的 responseId,這個時候 responseId 應(yīng)該是不存在的场晶,因為這時候 js 傳遞過來的數(shù)據(jù)就沒有 responseId混埠,可以回到 _doSend 方法看一下,Message 對象只存儲了callbackId诗轻,所以會走到 else 里岔冀,在這里取 callbackId 判斷是否為空,如果不為空概耻,說明 JS 在調(diào)用 Native 方法后還需要 Native 這邊的 “反饋”,在 callHandler 是就說了第三個參數(shù) responseCallBack 是用來接受 Native 返回來數(shù)據(jù)的回調(diào)罐呼, 那么這時候?qū)崿F(xiàn)了 Native 外部 registerHandler 該方法的 CallBackFunction 接口鞠柄,把 data 數(shù)據(jù)封裝成 Message 對象,給 Message 對象設(shè)置了一個 responseId, 接著調(diào)用了 queueMessage(responseMsg); 如果為空就說明 JS 不需要 Native 的數(shù)據(jù)回調(diào)嫉柴,而CallBackFunction也是空實現(xiàn)厌杜,最終根據(jù) handlerName 取出來 Native 對應(yīng)注冊的方法,把數(shù)據(jù)傳給上層计螺,這樣上層的registerHandler方法就收到 JS 傳過來的 data 數(shù)據(jù)了夯尽。

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 如果有回調(diào)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執(zhí)行
                        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 的數(shù)據(jù)回調(diào)登馒,接著看 queueMessage(responseMsg) , 直接調(diào)了 dispatchMessage匙握,在 dispatchMessage 中,把 Message 轉(zhuǎn)換 Json 字符串陈轿,拼接了 JS指令 圈纺,通過 webview.loadUrl() 調(diào)用了 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字符串轉(zhuǎn)義特殊字符
    messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
    messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
    messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'");
    String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
    // 必須要找主線程才會將數(shù)據(jù)傳遞出去 --- 劃重點
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        this.loadUrl(javascriptCommand);
    }
}

_handleMessageFromNative 調(diào)用了 _dispatchMessageFromNative 方法,同樣這段代碼也很長麦射,仔細看的話蛾娶,這段 JS 代碼和 Native 的 CallBackFunction 里的代碼邏輯上很像,這里依然只會走一部分代碼潜秋,在這里也會先取Message對象里的 responseId, 很明顯Native 傳過來的 Message 對象中有 responseId蛔琅,所以在這里就回調(diào)給了上層,callHandler 中的 responseCallBack 回調(diào)就有值了峻呛。下面的 else 也不會執(zhí)行罗售。

  //提供給native調(diào)用,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 {
            //直接發(fā)送
            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 調(diào)用 Native 方法并且得到 Native 返回的數(shù)據(jù)回調(diào),整個流程就走了一遍钩述,反過來其實也是一樣的莽囤,這里就不再跟進了,可以參考下面這張圖自己屢一下切距。剛才說的兩段比較長的代碼中朽缎,其實兩邊調(diào)用都走了這兩段代碼,每次單項調(diào)用就會走方法中對應(yīng)的某一部分。


native_js_native.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末话肖,一起剝皮案震驚了整個濱河市北秽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌最筒,老刑警劉巖贺氓,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異床蜘,居然都是意外死亡辙培,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門邢锯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扬蕊,“玉大人,你說我怎么就攤上這事丹擎∥惨郑” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵蒂培,是天一觀的道長再愈。 經(jīng)常有香客問我,道長护戳,這世上最難降的妖魔是什么翎冲? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮媳荒,結(jié)果婚禮上府适,老公的妹妹穿的比我還像新娘。我一直安慰自己肺樟,他們只是感情好檐春,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著么伯,像睡著了一般疟暖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上田柔,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天俐巴,我揣著相機與錄音,去河邊找鬼硬爆。 笑死欣舵,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的缀磕。 我是一名探鬼主播缘圈,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼劣光,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了糟把?” 一聲冷哼從身側(cè)響起绢涡,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎遣疯,沒想到半個月后雄可,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡缠犀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年数苫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辨液。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡虐急,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出室梅,到底是詐尸還是另有隱情,我是刑警寧澤疚宇,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布亡鼠,位于F島的核電站,受9級特大地震影響敷待,放射性物質(zhì)發(fā)生泄漏间涵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一榜揖、第九天 我趴在偏房一處隱蔽的房頂上張望勾哩。 院中可真熱鬧,春花似錦举哟、人聲如沸思劳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽潜叛。三九已至,卻和暖如春壶硅,著一層夾襖步出監(jiān)牢的瞬間威兜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工庐椒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留椒舵,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓约谈,卻偏偏與公主長得像笔宿,于是被迫代替她去往敵國和親犁钟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容