jsBridge原理解析

導(dǎo)語

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

一、WebView與js交互

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

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

二、WebView的js對象注入漏洞

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

三、jsBridge源碼分析

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

//一些初始化代碼就不展示了
······································
// 第一個參數(shù)在本地注冊一個叫"submitFromWeb"的方法供H5調(diào)用,
// 第二個參數(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");
            }
        });

// 第一個參數(shù)是H5頁面注冊的一個名為"functionInJs"的方法
// 第二個參數(shù)是客戶端本地傳給H5的字符串
// 第三個參數(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);// 每個回調(diào)接口都對應(yīng)一個key值(也就是你命名的方法名)
        }
    }

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

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

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

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

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

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

// BridgeWebViewClient.java
public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // url開頭是否是"yy://return/_fetchQueue/",說明是H5要返回數(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) {
                    // 先不要看這里,因為代碼還沒走到這一步,等回調(diào)的時候才會走這里,下面會有提示再來看。(省略部分代碼)
                    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方法則會有responseId這個值,也就是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)用客戶端會走這里
                            CallBackFunction responseFunction = null;
                            final String callbackId = m.getCallbackId();// 一般情況下都是有callbackId的,這是H5那邊設(shè)置的
                            if (!TextUtils.isEmpty(callbackId)) {
                                // 這里實現(xiàn)的回調(diào)接口是提供給客戶端再次去和H5交互的機(jī)會,對應(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)第二個參數(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)客戶端時只是實現(xiàn)一個回調(diào)方法(當(dāng)然這個回調(diào)方法非常重要),然后用鍵值對的方式存儲之后供下次配對。客戶端會再一次loadUrl加載本地js文件中的_fetchQueue()方法:

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

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

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

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

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

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

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

總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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