jsBridge原理解析

導(dǎo)語

現(xiàn)在大多數(shù)App與H5的交互越來越多,jsBridge是一個(gè)能使webView和js交互的通信方式,本文只對https://github.com/lzyzsd/JsBridge(以下涉及到的jsBridge源碼都是出自這個(gè)框架)進(jìn)行分析甫菠,只要你懂得了其中的原理册倒,你也可以封裝一個(gè)jsBridge。不過在介紹jsBridge的原理前思犁,我會(huì)簡單介紹下原始的webView與js交互以及為什么要用jsBridge。

一进肯、WebView與js交互

原始的js交互非常簡單容易理解激蹲,直接給出一段客戶端的代碼。

//開啟支持js交互
mWebView.getSettings().setJavaScriptEnabled(true);  
//添加js回調(diào)接口江掩,第一個(gè)參數(shù)是我們本地寫的一個(gè)專門提供方法給H5的js對象学辱;第二個(gè)參數(shù)是雙方規(guī)定好的命名,只有注冊的名稱和H5那邊對應(yīng)才可交互环形。
mWebView.addJavascriptInterface(new JSRequest(), "jsRequest");  
class JSRequest{
    @JavascriptInterface  //只有加了這個(gè)注解的方法才能被h5調(diào)用
    public void actionFromH5(){
          Log.v("JSRequest","H5調(diào)用了該方法");
    }
}
// 本地調(diào)用H5的方法用loadUrl實(shí)現(xiàn)策泣,actionFromNative是在H5里實(shí)現(xiàn)的一個(gè)方法
mWebView.loadUrl("javascript:actionFromNative()");

二、WebView的js對象注入漏洞

webView的js對象注入的方式非常簡單抬吟,可是為什么建議使用jsBridge呢萨咕?因?yàn)樵摲绞酱嬖诎踩[患。上述提到本地方法加了@JavascriptInterface注解才能被h5調(diào)用火本,這個(gè)是在Android4.2之后加的危队,是為了避免惡意js代碼獲取本地信息聪建,如SD卡中的用戶信息。但是@JavascriptInterface無法兼容4.2以前的版本茫陆,所以4.2之前的系統(tǒng)都有被隨時(shí)侵入獲取信息的可能金麸。
那么js是如何做到的?答案是反射簿盅。4.2之前沒有加@JavascriptInterface的情況下挥下,js是可以通過你注入的js對象(addJavascriptInterface的第一個(gè)參數(shù))直接拿到getClass(這個(gè)方法是基類Object的方法),然后再拿到Runtime對象用來執(zhí)行一些命令桨醋。原理大概就是這樣棚瘟,如果想具體了解如何實(shí)現(xiàn)的,請閱讀WebView的Js對象注入漏洞解決方案讨盒。

三解取、jsBridge源碼分析

jsBridge的最大作用就是解決了WebView的安全隱患,任何版本的系統(tǒng)都是適用的返顺。還是一樣禀苦,下面先介紹下jsBridge的用法,一些配置我就不介紹了遂鹊,直接拿主干部分振乏。

//一些初始化代碼就不展示了
······································
// 第一個(gè)參數(shù)在本地注冊一個(gè)叫"submitFromWeb"的方法供H5調(diào)用,
// 第二個(gè)參數(shù)是實(shí)現(xiàn)了BridgeHandler接口的匿名類用來回調(diào)秉扑。
webView.registerHandler("submitFromWeb", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                // 這里的data是H5傳給本地的數(shù)據(jù)慧邮,function.onCallBack是回調(diào)給H5的字符串?dāng)?shù)據(jù)
                Log.i(TAG, "handler = submitFromWeb, data from web = " + data);
                function.onCallBack("submitFromWeb exe, response data 中文 from Java");
            }
        });

// 第一個(gè)參數(shù)是H5頁面注冊的一個(gè)名為"functionInJs"的方法
// 第二個(gè)參數(shù)是客戶端本地傳給H5的字符串
// 第三個(gè)參數(shù)是實(shí)現(xiàn)回調(diào)接口的匿名內(nèi)部類
webView.callHandler("functionInJs", "data from Java", new CallBackFunction() {
                @Override
                public void onCallBack(String data) {
                    // TODO Auto-generated method stub
                    // data是H5返回給客戶端的數(shù)據(jù)
                    Log.i(TAG, "reponse data from js " + data);
                }
            });

3.1 H5調(diào)客戶端

jsBridge的源碼是很少的,理解起來不是那么困難舟陆,只要一步步往下走就好了误澳,首先我們從registerHandler出發(fā):

// BridgeWebView.java
public void registerHandler(String handlerName, BridgeHandler handler) {
        if (handler != null) {
            messageHandlers.put(handlerName, handler);// 每個(gè)回調(diào)接口都對應(yīng)一個(gè)key值(也就是你命名的方法名)
        }
    }

registerHandler方法就是這么簡單,客戶端操作已經(jīng)到此結(jié)束了秦躯。我認(rèn)為jsBridge最神奇的地方就是WebViewJavaScriptBridge.js這個(gè)js文件忆谓,對于不熟悉H5開發(fā)的同學(xué)可能有點(diǎn)看不懂(包括我),但是其實(shí)這個(gè)js文件的內(nèi)容和BridgeWebView.java非常類似踱承,大概看懂幾個(gè)重要方法的作用即可倡缠。下面是一段H5調(diào)用客戶端方法的代碼:

// demo.html
// testClick1方法是H5頁面點(diǎn)擊某個(gè)按鈕觸發(fā)的,然后會(huì)調(diào)客戶端的方法茎活。
function testClick1() {
            // call native method
            // 第一個(gè)參數(shù)是客戶端命名的方法
            // 第二個(gè)參數(shù)是傳給客戶端的數(shù)據(jù)
            // 第三個(gè)參數(shù)是客戶端返回?cái)?shù)據(jù)給H5的回調(diào)方法
            window.WebViewJavascriptBridge.callHandler(
                'submitFromWeb'
                , {'param': '中文測試'}
                , function(responseData) {
                    document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
                }
            );
        }

但是這個(gè)callHandler方法不是H5寫的昙沦,而是客戶端本地的WebViewJavaScriptBridge.js文件里的方法,這個(gè)文件里的內(nèi)容是直接可以注入到H5頁面(不得不感嘆H5的方便之處)载荔。

// WebViewJavaScriptBridge.js
// 提供給H5的js方法
function callHandler(handlerName, data, responseCallback) {
        _doSend({
            handlerName: handlerName,
            data: data
        }, responseCallback);
    }
// 對應(yīng)上面方法里的_doSend盾饮,在發(fā)送消息隊(duì)列中加入消息,觸發(fā)native請求
function _doSend(message, responseCallback) {
        // responseCallback按命名理解就是響應(yīng)回調(diào),也就是說是客戶端再傳數(shù)據(jù)給H5的時(shí)候用到的
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message.callbackId = callbackId;
        }
        sendMessageQueue.push(message);

        // 我的理解是這行代碼會(huì)觸發(fā)WebViewClient中的shouldOverrideUrlLoading丐谋,這是交互的關(guān)鍵點(diǎn)
        // 返回給客戶端的url是"yy://__QUEUE_MESSAGE__/"
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

上面的注釋已經(jīng)寫明H5最終會(huì)觸發(fā)WebViewClient中的shouldOverrideUrlLoading:

// BridgeWebViewClient.java
public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // url開頭是否是"yy://return/_fetchQueue/"芍碧,說明是H5要返回?cái)?shù)據(jù)給客戶端了
            webView.handlerReturnData(url);
            return true;
        } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //url開頭是否是"yy://__QUEUE_MESSAGE__/"煌珊,說明H5要調(diào)用客戶端了号俐。
            webView.flushMessageQueue();
            return true;
        } else {
            return super.shouldOverrideUrlLoading(view, url);
        }
    }

上面的代碼已經(jīng)走到H5調(diào)用客戶端了,接下去跟進(jìn)webView.flushMessageQueue()看看:

// BridgeWebView.java
void flushMessageQueue() {
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
                @Override
                public void onCallBack(String data) {
                    // 先不要看這里定庵,因?yàn)榇a還沒走到這一步吏饿,等回調(diào)的時(shí)候才會(huì)走這里,下面會(huì)有提示再來看蔬浙。(省略部分代碼)
                    List<Message> list = null;
                    try {
                        list = Message.toArrayList(data);// 解析H5傳過來的Json數(shù)據(jù)
                    } catch (Exception e) {
                        e.printStackTrace();
                        return;
                    }
                    for (int i = 0; i < list.size(); i++) {
                        Message m = list.get(i);
                        String responseId = m.getResponseId();
                        // 如果是客戶端調(diào)用H5方法則會(huì)有responseId這個(gè)值猪落,也就是webView.callHandler
                        if (!TextUtils.isEmpty(responseId)) {
                            CallBackFunction function = responseCallbacks.get(responseId);
                            String responseData = m.getResponseData();
                            function.onCallBack(responseData);// 回調(diào)到webView.callHandler里面的回調(diào)方法
                            responseCallbacks.remove(responseId);
                        } else {// H5調(diào)用客戶端會(huì)走這里
                            CallBackFunction responseFunction = null;
                            final String callbackId = m.getCallbackId();// 一般情況下都是有callbackId的,這是H5那邊設(shè)置的
                            if (!TextUtils.isEmpty(callbackId)) {
                                // 這里實(shí)現(xiàn)的回調(diào)接口是提供給客戶端再次去和H5交互的機(jī)會(huì)畴博,對應(yīng)webView.registerHandler(name,handler)里面的function
                                responseFunction = new CallBackFunction() {
                                    @Override
                                    public void onCallBack(String data) {
                                        Message responseMsg = new Message();
                                        responseMsg.setResponseId(callbackId);// js傳過來的callbackId賦值給responseId回傳給js笨忌,這樣就可以配對了。
                                        responseMsg.setResponseData(data);
                                        queueMessage(responseMsg);// 向H5發(fā)送消息
                                    }
                                };
                            }
                            BridgeHandler handler;
                            if (!TextUtils.isEmpty(m.getHandlerName())) {
                                handler = messageHandlers.get(m.getHandlerName());
                            } 
                            if (handler != null){// 客戶端只有registerHandler后取出來的handler才不為null
                                // 這一步就是調(diào)到了webView.registerHandler(name,handler)第二個(gè)參數(shù)BridgeHandler里了
                                handler.handler(m.getData(), responseFunction);
                            }
                        }
                }  
             }
         }
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
        this.loadUrl(jsUrl); // 加載jsUrl="javascript:WebViewJavascriptBridge._fetchQueue();"
        // 鍵值對形式存放響應(yīng)回調(diào)接口俱病,這里的key是"_fetchQueue"
        responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}

現(xiàn)在整理下發(fā)現(xiàn)H5第一次調(diào)客戶端時(shí)只是實(shí)現(xiàn)一個(gè)回調(diào)方法(當(dāng)然這個(gè)回調(diào)方法非常重要)官疲,然后用鍵值對的方式存儲(chǔ)之后供下次配對×料叮客戶端會(huì)再一次loadUrl加載本地js文件中的_fetchQueue()方法:

// WebViewJavaScriptBridge.js
function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        // 會(huì)觸發(fā)客戶端的shouldOverrideUrlLoading途凫,傳遞url的形式是:"yy://return/_fetchQueue/"+H5給客戶端的數(shù)據(jù)
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}

shouldOverrideUrlLoading的代碼已經(jīng)在之前貼出過,這里就不再貼出溢吻,然后會(huì)調(diào)用webView.handlerReturnData(url):

void handlerReturnData(String url) {
        String functionName = BridgeUtil.getFunctionFromReturnUrl(url);// 拿到functionName="_fetchQueue"
        CallBackFunction f = responseCallbacks.get(functionName);// 拿到的key就是為配對鍵值對的拔选!還記得上面存儲(chǔ)過了嗎促王?
        String data = BridgeUtil.getDataFromReturnUrl(url);// 拿到H5給客戶端的數(shù)據(jù)
        if (f != null) {
            f.onCallBack(data);// 回調(diào)
            responseCallbacks.remove(functionName);
            return;
        }
}

f.onCallBack(data)就是在flushMessageQueue實(shí)現(xiàn)的那個(gè)回調(diào)方法啊犀盟,所以這個(gè)時(shí)候就要回去看看那個(gè)方法里面具體做了什么(重點(diǎn)已注釋),到此為止H5調(diào)客戶端的方法流程基本已經(jīng)走完蝇狼,queueMessage(responseMsg)方法就不再具體講了阅畴,作用就是向H5發(fā)消息(類似于客戶端調(diào)用H5方法,但是有區(qū)別)题翰。

3.2 客戶端調(diào)用H5方法

我覺得再從源碼一步步講解是沒什么意義的恶阴,只要理解了H5調(diào)用客戶端方法就可以了,因?yàn)榱鞒毯虷5調(diào)用客戶端方法是相反的豹障,也就是說WebViewJavaScriptBridge.js和BridgeWebView.java是功能相似的不同語言所寫的文件冯事,接下來我通過一張流程圖過一遍客戶端調(diào)用H5方法 :

客戶端調(diào)H5方法.png

總結(jié)

現(xiàn)在的App開發(fā)熟練使用WebView以及和js交互是很有必要的,jsBridge的實(shí)現(xiàn)也不復(fù)雜血公,只要和H5定好協(xié)議昵仅,完全可以自己寫一個(gè)jsBridge通信方式的框架。而且多閱讀源碼有助于自己的提升,從這些簡單而精妙的源碼入手是再合適不過了摔笤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末够滑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子吕世,更是在濱河造成了極大的恐慌彰触,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件命辖,死亡現(xiàn)場離奇詭異况毅,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)尔艇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門尔许,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人终娃,你說我怎么就攤上這事味廊。” “怎么了棠耕?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵余佛,是天一觀的道長。 經(jīng)常有香客問我昧辽,道長衙熔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任搅荞,我火速辦了婚禮红氯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘咕痛。我一直安慰自己痢甘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布茉贡。 她就那樣靜靜地躺著塞栅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腔丧。 梳的紋絲不亂的頭發(fā)上放椰,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音愉粤,去河邊找鬼砾医。 笑死,一個(gè)胖子當(dāng)著我的面吹牛衣厘,可吹牛的內(nèi)容都是我干的如蚜。 我是一名探鬼主播压恒,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼错邦!你這毒婦竟也來了探赫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤撬呢,失蹤者是張志新(化名)和其女友劉穎伦吠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體倾芝,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡讨勤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晨另。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谱姓,死狀恐怖借尿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屉来,我是刑警寧澤路翻,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站茄靠,受9級(jí)特大地震影響茂契,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜慨绳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一掉冶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧脐雪,春花似錦厌小、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至脂信,卻和暖如春癣蟋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狰闪。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工疯搅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人尝哆。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓秉撇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子琐馆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

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

  • 前言 總結(jié) Android WebView 常用的相關(guān)知識(shí)點(diǎn)规阀,令包含以下干貨內(nèi)容分析:Js注入漏洞、WebView...
    無名小子的雜貨鋪閱讀 69,800評論 17 169
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,099評論 25 707
  • 更多文章請關(guān)注:開發(fā)者技術(shù)前線 Android開發(fā)目前現(xiàn)狀來說瘦麸,開發(fā)者大部分時(shí)間花在UI的屏幕適配上谁撼,使用原生控件...
    Tamic閱讀 17,624評論 15 80
  • 宋.黃庭堅(jiān) 瑤草一何碧,春入武陵溪滋饲。溪上桃花無數(shù)厉碟,花上有黃鸝。我欲穿花尋路屠缭,直入白云深處箍鼓,浩氣展虹霓。只恐花深里呵曹,...
    雨_蓮閱讀 227評論 0 0
  • 如果把書的難易程度分為10級(jí)的話奄喂,這本墨傾城寫的《三毛傳》可以1級(jí)铐殃,沒有難度,而且到處都是修辭的堆砌跨新。 讓人一讀的...
    天外來客人閱讀 2,537評論 0 5