JsBridge:實(shí)現(xiàn)JavaScript和Java的互相調(diào)用

1. Js調(diào)用Java涝焙,Java調(diào)用Js

在Android開(kāi)發(fā)中,能實(shí)現(xiàn)Js調(diào)用Java蒂教,有4種方法:
1.JavascriptInterface
2.WebViewClient.shouldOverrideUrlLoading()
3.WebChromeClient.onConsoleMessage()
4.WebChromeClient.onJsPrompt()

1.1 JavascriptInterface

這是Android提供的Js與Native通信的官方解決方案弹沽。
首先Java代碼要實(shí)現(xiàn)這么一個(gè)類,它的作用是提供給Js調(diào)用溶其。

public class JavascriptInterface {

    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
    }
}

然后把這個(gè)類添加到WebView的JavascriptInterface中骚腥。webView.addJavascriptInterface(new JavascriptInterface(), “javascriptInterface”); 在Js代碼中就能直接通過(guò)“javascriptInterface”直接調(diào)用了該Native的類的方法。

function showToast(toast) {
    javascript:javascriptInterface.showToast(toast);
}

但是這個(gè)官方提供的解決方案在Android4.2之前存在嚴(yán)重的安全漏洞瓶逃。在Android4.2之后束铭,加入了@JavascriptInterface才得到解決。所以考慮到兼容低版本的系統(tǒng)金闽,JavascriptInterface并不適合纯露。

1.2 WebViewClient.shouldOverrideUrlLoading()

這個(gè)方法的作用是攔截所有WebView的Url跳轉(zhuǎn)。頁(yè)面可以構(gòu)造一個(gè)特殊格式的Url跳轉(zhuǎn)代芜,shouldOverrideUrlLoading攔截Url后判斷其格式埠褪,然后Native就能執(zhí)行自身的邏輯了。

public class CustomWebViewClient extends WebViewClient {

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (isJsBridgeUrl(url)) {
            // JSbridge的處理邏輯
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
}

1.3 WebChromeClient.onConsoleMessage()

這是Android提供給Js調(diào)試在Native代碼里面打印日志信息的API挤庇,同時(shí)這也成了其中一種Js與Native代碼通信的方法钞速。在Js代碼中調(diào)用console.log(‘xxx’)方法。

console.log('log message that is going to native code')

就會(huì)在Native代碼的WebChromeClient.consoleMessage()中得到回調(diào)嫡秕。consoleMessage.message()獲得的正是Js代碼console.log(‘xxx’)的內(nèi)容渴语。

public class CustomWebChromeClient extends WebChromeClient {

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        super.onConsoleMessage(consoleMessage);
        String msg = consoleMessage.message();//JavaScript輸入的Log內(nèi)容
    }
}

1.4 WebChromeClient.onJsPrompt()

其實(shí)除了WebChromeClient.onJsPrompt(),還有WebChromeClient.onJsAlert()和WebChromeClient.onJsConfirm()昆咽。顧名思義驾凶,這三個(gè)Js給Native代碼的回調(diào)接口的作用分別是展示提示信息,展示警告信息和展示確認(rèn)信息掷酗。鑒于调违,alert和confirm在Js的使用率很高,所以JSBridge的解決方案中都傾向于選用onJsPrompt()泻轰。
Js中調(diào)用

window.prompt(message, value)

WebChromeClient.onJsPrompt()就會(huì)受到回調(diào)技肩。onJsPrompt()方法的message參數(shù)的值正是Js的方法window.prompt()的message的值。

public class CustomWebChromeClient extends WebChromeClient {

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 處理JS 的調(diào)用邏輯
        result.confirm();
        return true;
    }
}

1.5 Java調(diào)用Js

前文提到的4種通信方式都是Js通信Native的Java浮声,而反過(guò)來(lái)虚婿,Java通信Js只有一種方式旋奢。那就是調(diào)用WebView.loadUrl()去執(zhí)行一個(gè)預(yù)先定義好的Js方法。

webView.loadUrl(String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative(%s)", data));

2. JsBridge

接下來(lái)會(huì)結(jié)合JsBridge這個(gè)開(kāi)源組件(https://github.com/lzyzsd/JsBridge)來(lái)講解一下JsBridge的原理

2.1 Java調(diào)用Js的functionInJs方法的流程圖

image

2.2 JsBridge的UML圖

image

2.3 JsBridge工作步驟講解

2.3.1 WebView加載html頁(yè)面

webView.registerHandler(“submitFromWeb”, …);這是Java層注冊(cè)了一個(gè)叫”submitFromWeb”的接口方法然痊,目的是提供給Js來(lái)調(diào)用至朗。這個(gè)”submitFromWeb”的接口方法的回調(diào)就是BridgeHandler.handler()。webView.callHandler(“functionInJs”, …, new CallBackFunction());這是Java層主動(dòng)調(diào)用Js的”functionInJs”方法玷过。

public class MainActivity extends Activity implements OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        webView = (BridgeWebView) findViewById(R.id.webView);
        webView.loadUrl("file:///android_asset/demo.html");
        webView.registerHandler("submitFromWeb", new BridgeHandler() {

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

        });

        webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {
            @Override
            public void onCallBack(String data) {

            }
        });
    }
}

我們一層層深入callHandler()方法的實(shí)現(xiàn)爽丹。這其中會(huì)調(diào)用到doSend()方法,這里想解釋下m.setCallbackId(callbackStr)方法的作用辛蚊。該方法設(shè)置的callbackId生成后不僅僅會(huì)被傳到Js粤蝎,而且會(huì)以key-value對(duì)的形式和responseCallback配對(duì)保存到responseCallbacks這個(gè)Map里面。它的目的袋马,就是為了等Js把處理結(jié)果回調(diào)給Java層后初澎,Java層能根據(jù)callbackId找到對(duì)應(yīng)的responseCallback,做后續(xù)的回調(diào)處理虑凛。

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

最終可以看到是BridgeWebView.dispatchMessage(Message m)方法調(diào)用的是this.loadUrl()碑宴,調(diào)用了_handleMessageFromNative這個(gè)Js方法。那這個(gè)Js的方法是哪里來(lái)的呢桑谍?

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

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

2.3.2 加載WebViewJavascriptBridge.js

在WebViewClient.onPageFinished()里面的BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs)延柠。正是把保存在assert/WebViewJavascriptBridge.js加載到WebView中。

package com.github.lzyzsd.jsbridge;

public class BridgeWebViewClient extends WebViewClient {

    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);

        if (BridgeWebView.toLoadJs != null) {
            BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
        }

        //
        if (webView.getStartupMessage() != null) {
            for (Message m : webView.getStartupMessage()) {
                webView.dispatchMessage(m);
            }
            webView.setStartupMessage(null);
        }
    }
}

2.3.3 分析WebViewJavascriptBridge.js

我們看看WebViewJavascriptBridge.js的代碼锣披,就能找到function _handleMessageFromNative()這個(gè)Js方法了贞间。_handleMessageFromNative()方法里面會(huì)調(diào)用_dispatchMessageFromNative()方法。當(dāng)處理來(lái)自Java層的主動(dòng)調(diào)用時(shí)候會(huì)走“直接發(fā)送”的else分支雹仿。message.callbackId會(huì)被取出來(lái)增热,實(shí)例化一個(gè)responseCallback,而它是用來(lái)Js處理完成后把結(jié)果數(shù)據(jù)回調(diào)給Java層代碼的胧辽。接著會(huì)根據(jù)message.handleName(在這個(gè)分析例子中峻仇,handleName的值就是”functionInJs”)在messageHandlers這個(gè)Map去獲取handler,最后交給handler去處理邑商。

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

2.3.4 頁(yè)面Html注冊(cè)”functionInJs”方法

延續(xù)上面的分析摄咆,messageHandler是哪里設(shè)置的呢。答案就在當(dāng)初webView.loadUrl(“file:///android_asset/demo.html”);加載的這個(gè)demo.html中人断。bridge.registerHandler(“functionInJs”, …)這里注冊(cè)了”functionInJs”豆同。

<html>
    <head>
    ...
    </head>
    <body>
    ...
    </body>
    <script>
        ...

        connectWebViewJavascriptBridge(function(bridge) {
            bridge.init(function(message, responseCallback) {
                console.log('JS got a message', message);
                var data = {
                    'Javascript Responds': '測(cè)試中文!'
                };
                console.log('JS responding with', data);
                responseCallback(data);
            });

            bridge.registerHandler("functionInJs", function(data, responseCallback) {
                document.getElementById("show").innerHTML = ("data from Java: = " + data);
                var responseData = "Javascript Says Right back aka!";
                responseCallback(responseData);
            });
        })
    </script>
</html>

2.3.5 “functionInJs”執(zhí)行結(jié)果回傳Java

“funciontInJs”執(zhí)行完畢后調(diào)用的responseCallback正是_dispatchMessageFromNative()實(shí)例化的,而它實(shí)際會(huì)調(diào)用_doSend()方法含鳞。_doSend()方法會(huì)先把Message推送到sendMessageQueue中。然后修改messagingIframe.src芹务,這里會(huì)出發(fā)Java層的WebViewClient.shouldOverrideUrlLoading()的回調(diào)蝉绷。

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()里面鸭廷,會(huì)先執(zhí)行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)) { // 如果是返回?cái)?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);
    }
}

webView.flushMessageQueue()首先去執(zhí)行Js的_flushQueue()方法熔吗,并附帶著CallBackFunction辆床。Js的_flushQueue()方法會(huì)把sendMessageQueue中的所有message都回傳給Java層。CallBackFunction就是把messageQueue解析出來(lái)后一個(gè)一個(gè)Message在for循環(huán)中處理桅狠,也正是在for循環(huán)中讼载,”functionInJs”的Java層回調(diào)方法被執(zhí)行了。

void flushMessageQueue() {
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, 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++) {
                    ...
                }
            }
        });
    }
}

到此中跌,JsBridge的調(diào)用流程就分析完畢了咨堤。雖然JsBridge使用了MessageQueue后,分析起來(lái)有點(diǎn)繞漩符。但原理是不變的一喘,Js調(diào)用Java是通過(guò)WebViewClient.shouldOverrideUrlLoading()。當(dāng)然嗜暴,還有在文章開(kāi)頭介紹另外3種方式凸克。Java調(diào)用Js是通過(guò)WebView.loadUrl(“javascript:xxxx”)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闷沥,一起剝皮案震驚了整個(gè)濱河市萎战,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌舆逃,老刑警劉巖蚂维,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異颖侄,居然都是意外死亡鸟雏,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門览祖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)孝鹊,“玉大人,你說(shuō)我怎么就攤上這事展蒂∮只睿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵锰悼,是天一觀的道長(zhǎng)柳骄。 經(jīng)常有香客問(wèn)我,道長(zhǎng)箕般,這世上最難降的妖魔是什么耐薯? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上曲初,老公的妹妹穿的比我還像新娘体谒。我一直安慰自己,他們只是感情好臼婆,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布抒痒。 她就那樣靜靜地躺著,像睡著了一般颁褂。 火紅的嫁衣襯著肌膚如雪故响。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,816評(píng)論 1 290
  • 那天颁独,我揣著相機(jī)與錄音彩届,去河邊找鬼。 笑死奖唯,一個(gè)胖子當(dāng)著我的面吹牛惨缆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丰捷,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼坯墨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了病往?” 一聲冷哼從身側(cè)響起捣染,我...
    開(kāi)封第一講書(shū)人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎停巷,沒(méi)想到半個(gè)月后耍攘,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡畔勤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年蕾各,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庆揪。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡式曲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缸榛,到底是詐尸還是另有隱情吝羞,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布内颗,位于F島的核電站钧排,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏均澳。R本人自食惡果不足惜恨溜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一符衔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧糟袁,春花似錦柏腻、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)颗品。三九已至肯尺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間躯枢,已是汗流浹背则吟。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锄蹂,地道東北人氓仲。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像得糜,于是被迫代替她去往敵國(guó)和親敬扛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

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

  • 隨著H5性能的提升朝抖,在我們移動(dòng)應(yīng)用開(kāi)發(fā)的過(guò)程中啥箭,我們會(huì)越來(lái)越多的在我們的App頁(yè)面內(nèi)嵌入H5頁(yè)面,使得App變的更...
    Jensen95閱讀 4,199評(píng)論 0 21
  • 前言 總結(jié) Android WebView 常用的相關(guān)知識(shí)點(diǎn)治宣,令包含以下干貨內(nèi)容分析:Js注入漏洞急侥、WebView...
    無(wú)名小子的雜貨鋪閱讀 69,779評(píng)論 17 169
  • JSBridge 1. Why do we need JSBridge? 2. Why is “JS”Bridge...
    loveqin閱讀 9,153評(píng)論 0 7
  • 在Android中,JSBridge已經(jīng)不是什么新鮮的事物了侮邀,各家的實(shí)現(xiàn)方式也略有差異。大多數(shù)人都知道WebVie...
    Jannonx閱讀 1,316評(píng)論 0 5
  • 1.今天一早幫助阿姨打掃會(huì)議室的衛(wèi)生,種下了幫助別人的種子榜苫。 2.為日行一善放生群及騰訊公益保護(hù)兒童安全各捐助了一...
    照法閱讀 141評(píng)論 0 0