DSBridge-Android 源碼分析

一 Android WebView Js 原生API

Android WebView 提供了Js 和 WebView相互調(diào)用的接口雁仲,js 調(diào)用Android 代碼通過

  1. @JavascriptInterface 注解
  2. WebView.addJavascriptInterface(Object object, String name) 方法

實(shí)現(xiàn)JS 和java 對象的映射。

同樣 WebView 也提供了 java 調(diào)用Js 代碼的機(jī)制鹃答。通過以下兩個(gè)方法:

  1. WebView.evaluateJavascript(String script, ValueCallback<String> resultCallback);
  2. WebView.loadUrl(String script); Android 4.4 以下版本使用
private void evaluateJavascript(String script) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        WebView.evaluateJavascript(String script, ValueCallback<String> resultCallback);
    } else {
        WebView.loadUrl(String script);
    }
}

二 DSBridge 分析

github 上提供了一個(gè)Js Bridage, DSBridge-Android干厚, 分析下實(shí)現(xiàn)原理:

一共三個(gè)java 文件:

文件 功能
DWebView.java 繼承WebView 封裝了Js調(diào)用
CompletionHandler.java 處理異步請求使用
OnReturnValue.java 返回值 接口

DWebView 類 繼承自 WebView 主要包括這幾個(gè)函數(shù)

  1. init 在WebView 的構(gòu)造函數(shù)中調(diào)動(dòng)李滴,完成一些WebView 的設(shè)置螃宙。
  2. injectJs
  3. evaluateJavascript(final String script);

1. init

init 函數(shù)中主要有兩個(gè)部分處理一個(gè)是注冊一個(gè) WebChromeClient, 一個(gè)是調(diào)用addJavascriptInterface 接口注冊一個(gè) Js 調(diào)用Java的通用api

  1. super.setWebChromeClient(mWebChromeClient); 在mWebChromeClient 的回調(diào)中調(diào)用 injectJs 完成Js 注入
  2. super.addJavascriptInterface(new Object(){}, BRIDGE_NAME)所坯,這是DSBridge的核心功能谆扎,向 Js 頁面注冊一個(gè)通用的Js 對象,這個(gè)對象有一個(gè) call 方法芹助,通過這個(gè)call 方法實(shí)現(xiàn)對其它 Android Api 的調(diào)用堂湖,下面主要分析這個(gè)方法。

2. js 調(diào)用方式

在 js 頁面調(diào)用 Andoid 代碼時(shí)通過:dsBridge 為java evaluateJavascript 調(diào)用是的Object 在Js的映射對象状土,然后調(diào)用 這個(gè)對象的call 方法:

// init dsBridge
<script src="https://unpkg.com/dsbridge/dist/dsbridge.js"> </script>
var dsBridge=require("dsbridge")

//Call synchronously 
var str=dsBridge.call("testSyn", {msg: "testSyn"});

//Call asynchronously
dsBridge.call("testAsyn", {msg: "testAsyn"}, function (v) {
  alert(v);
})

3. Java 注冊 js Api

java 代碼注冊无蜂,js 調(diào)用的函數(shù)都封裝在 JsApi 這個(gè)對象中,注意是DWebView 的 setJavascriptInterface蒙谓,不是原生WebView .

DWebView.setJavascriptInterface(new JsApi());

public class JsApi{

    @JavascriptInterface
    public void testAsyn(JSONObject jsonObject, CompletionHandler handler) throws JSONException {
        handler.complete(jsonObject.getString("msg")+" [ asyn call]");
    }
}

4. call 函數(shù)

call 需要使用 JavascriptInterface 注解注釋斥季,Js 中的三個(gè)參數(shù)被到這里被簡化為兩個(gè)參數(shù),原因在Js 代碼中分析累驮。

  1. methodName java 方法名
  2. args 參數(shù)酣倾, 注意String 格式其實(shí)是Json 字符串

具體過程看代碼注釋:

@JavascriptInterface
public String call(String methodName, String args) {
        String error = "Js bridge method called, but there is " +
            "not a JavascriptInterface object, please set JavascriptInterface object first!";
       
        // 首先檢查是否注冊了Js api 相關(guān)的對象    
        if (jsb == null) {
            Log.e("SynWebView", error);
            return "";
        }

        // 獲取注冊的Js api 對象的Class 對象
        Class<?> cls = jsb.getClass();
        try {
            Method method;
            // 異步標(biāo)記
            boolean asyn = false;    
            // String 類型的參數(shù)轉(zhuǎn)化為  Json 對象
            JSONObject arg = new JSONObject(args);
            String callback = "";
            try {
                // 檢查 json 對象中是否有_dscbstub 這個(gè)Key,如果有表示有回調(diào)Js的函數(shù),是一個(gè)異步調(diào)用谤专,
                // 然后移除躁锡,那么json對象中保存的都是參數(shù)
                // 有了對象,知道了對象的方法的String置侍,通過反射獲取這個(gè)方法映之。通過反射的參數(shù)可知
                // 方法的函數(shù)簽名為:xxxMethod(JSONObject object, CompletionHandler handler);
                callback = arg.getString("_dscbstub");
                arg.remove("_dscbstub");
                method = cls.getDeclaredMethod(methodName,
                        new Class[]{JSONObject.class, CompletionHandler.class});
                asyn = true;
            } catch (Exception e) {
                method = cls.getDeclaredMethod(methodName, new Class[]{JSONObject.class});
            }

            // 錯(cuò)誤檢查
            if (method == null) {
                error = "ERROR! \n Not find method \"" + methodName + "\" implementation! ";
                Log.e("SynWebView", error);
                evaluateJavascript(String.format("alert(decodeURIComponent(\"%s\"})", error));
                return "";
            }

            // Js 調(diào)用的API 需要使用 @JavascriptInterface 注解,
            // 在4.4 以前的平臺(tái)上有Js 安全漏洞蜡坊,通過這個(gè)注解檢查是否合法的API.
            // call 函數(shù)已經(jīng)用 @JavascriptInterface 標(biāo)注杠输,是一個(gè)合法的API。jsp 對象由于繞過了WebView
            // 的 @JavascriptInterface 注解檢查算色,需要手動(dòng)校驗(yàn)抬伺。
            JavascriptInterface annotation = method.getAnnotation(JavascriptInterface.class);
            if (annotation != null) {
                Object ret;
                // 設(shè)置方法為可訪問的
                method.setAccessible(true);
                if (asyn) {
                    // 異步調(diào)用, 講異步調(diào)用的邏輯封裝在CompletionHandler 中螟够,
                    // 使用閉包的方式實(shí)現(xiàn)callback.
                    final String cb = callback;
                    ret = method.invoke(jsb, arg, new CompletionHandler() {
                        灾梦、、妓笙、

                        //  可以再method 方法中調(diào)用這個(gè)函數(shù)若河,實(shí)現(xiàn)異步。
                        private void complete(String retValue,boolean complete) {
                            try {
                                // retValue 為 method  執(zhí)行的結(jié)果寞宫,complete 可以控制多次回調(diào)萧福。
                                // 將callback 和參數(shù)組合為 javascript 語句,然后通過evaluateJavascript 
                                // 方法調(diào)用js 執(zhí)行
                                if (retValue == null) retValue = "";
                                retValue = URLEncoder.encode(retValue, "UTF-8").replaceAll("\\+", "%20");
                                String script = String.format("%s(decodeURIComponent(\"%s\"));", cb, retValue);
                                // 將callback 方法從Html 的window 對象刪除辈赋,原因在js 代碼分析
                                if(complete) {
                                    script += "delete window."+cb;
                                }
                                evaluateJavascript(script);
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                } else {
                    // 同步調(diào)用
                    ret = method.invoke(jsb, arg);
                }
                if (ret == null) {
                    ret = "";
                }
                
                // 返回結(jié)果
                return ret.toString();
            } else {
                error = "Method " + methodName + " is not invoked, since  " +
                    "it is not declared with JavascriptInterface annotation! ";
                evaluateJavascript(String.format("alert('ERROR \\n%s')", error));
                Log.e("SynWebView", error);
            }
        } catch (Exception e) {
            evaluateJavascript(String.format("alert('ERROR! \\nCall failed:Function does not exist or parameter is invalid[%s]')", e.getMessage()));
            e.printStackTrace();
        }
        return "";
    }
    
    @Keep
    @JavascriptInterface
    public void returnValue(int id, String value) {
        OnReturnValue handler = handlerMap.get(id);
        if (handler != null) {
            handler.onValue(value);
            handlerMap.remove(id);
        }
    }
}, BRIDGE_NAME);            

5. injectJs 注入js 代碼

在WebChromeClient 的回調(diào)中鲫忍,會(huì)調(diào)用injectJs 方法注入js膏燕,onProgressChanged onReceivedTitle 保證在js 代碼運(yùn)行前 js注入完成

private WebChromeClient mWebChromeClient = new WebChromeClient() {

        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            injectJs();
            
        }

        @Override
        public void onReceivedTitle(WebView view, String title) {
            injectJs();
        }
}

    private void injectJs() {
        evaluateJavascript("function getJsBridge(){window._dsf=window._dsf||{};return{call:function(b,a,c){\"function\"==typeof a&&(c=a,a={});if(\"function\"==typeof c){window.dscb=window.dscb||0;var d=\"dscb\"+window.dscb++;window[d]=c;a._dscbstub=d}a=JSON.stringify(a||{});return window._dswk?prompt(window._dswk+b,a):\"function\"==typeof _dsbridge?_dsbridge(b,a):_dsbridge.call(b,a)},register:function(b,a){\"object\"==typeof b?Object.assign(window._dsf,b):window._dsf[b]=a}}}dsBridge=getJsBridge();");
    }

6. javascript 代碼注入分析

injectJs 調(diào)用的Js 代碼如下:

function getJsBridge() {
    // window 對象的 _dsf 賦值, 如果沒有定義過悟民, 則定義為 {}坝辫;
    // dsf 域用來保存 java 調(diào)用js 的function.
    window._dsf = window._dsf || {};
    
    // 返回 json 一個(gè)匿名json對象, json 對象包含兩個(gè)function:call 和 register。
    return {
        // call function 包含三個(gè)參數(shù)射亏, 方法名近忙, 參數(shù),回調(diào)函數(shù)智润,
        call: function (method, args, cb) {
            var ret = "";
            // 檢查第二個(gè)參數(shù)類型是否為 function , 如果為function 則表示為回調(diào)函數(shù)
            if (typeof args == "function") {
                cb = args;
                args = {}
            }
            
            // 這一步處理很有技巧及舍,在設(shè)置回調(diào)函數(shù)的時(shí)候,回調(diào)函數(shù)可能為匿名函數(shù)窟绷,
            // 在這里通過window 對象的一個(gè)域保存锯玛,避免垃圾回收和Java 回調(diào)的時(shí)候能夠找到。
            // args 對象中 回調(diào)函數(shù)的Key 被設(shè)置為"_dscbstub"兼蜈, java 中是根據(jù)這個(gè)名字找到的callback
            // 也解釋了java  中的call API 為兩個(gè)參數(shù)更振, Js 中為三個(gè)參數(shù)的原因。
            if (typeof cb == "function") {
                window.dscb = window.dscb || 0;
                var cbName = "dscb" + window.dscb++;
                window[cbName] = cb;
                args["_dscbstub"] = cbName
            }
            args = JSON.stringify(args || {});
            if (window._dswk) {
                // debug 分支饭尝, window 的  _dswk 域決定
                ret = prompt(window._dswk + method, args)
            } else {
                // _dsbridge 對象為java 中調(diào)
                addJavascriptInterface(new Object(){}, BRIDGE_NAME) 映射的JS 對象
                if (typeof _dsbridge == "function") {
                    ret = _dsbridge(method, args)
                } else {
                    // 我們的代碼走這里
                    ret = _dsbridge.call(method, args)
                }
            }
            return ret
        }, register: function (name, fun) {
            if (typeof name == "object") {
                Object.assign(window._dsf, name)
            } else {
                window._dsf[name] = fun
            }
        }
    }
}

// 最后把這個(gè)匿名對象掛在 window dsBridage 域下肯腕。
window.dsBridge = getJsBridge();

7 evaluateJavascript

DWebView 對 evaluateJavascript 做了兩次封裝,主要解決兩個(gè)問題:

  1. 在非主線程中調(diào)用的問題, 通過handler post 到主線程中處理钥平。
  2. 4.4 以前的版本兼容的問題实撒。
    public void evaluateJavascript(final String script) {
        if (Looper.getMainLooper() == Looper.myLooper()) {
            _evaluateJavascript(script);
        } else {
            Message msg=new Message();
            msg.what=EXEC_SCRIPT;
            msg.obj=script;
            mainThreadHandler.sendMessage(msg);
        }
    }
    
    private void _evaluateJavascript(String script) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            DWebView.super.evaluateJavascript(script, null);
        } else {
            loadUrl("javascript:" + script);
        }
    }    

8 java 調(diào)用js

android 的原生方式中java調(diào)用js 的接口已經(jīng)很完善了。DSBridge 使用callHandler涉瘾。js function 在調(diào)用前需要掛到
window._dsf 域下知态,參考 js代碼的 register 函數(shù)。

//Register javascript function for Native invocation
 dsBridge.register('addValue',function(l,r){
     return l+r;
 })

java 中調(diào)用前指明window._dsf 下的function立叛。 對代碼做了一個(gè)約束负敏。

DWebView.callHandler("addValue",new Object[]{1,"hello"},new OnReturnValue(){
       @Override
       public void onValue(String retValue) {
          Log.d("jsbridge","call succeed,return value is "+retValue);
       }
})


public void callHandler(String method, Object[] args, final OnReturnValue handler) {
        if (args == null) args = new Object[0];
        String arg = new JSONArray(Arrays.asList(args)).toString();
        String script = String.format("(window._dsf.%s||window.%s).apply(window._dsf||window,%s)", method,method, arg);
        if(handler!=null){
            script = String.format("%s.returnValue(%d,%s)",BRIDGE_NAME,callID, script);
            handlerMap.put(callID++, handler);
        }
        evaluateJavascript(script);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市秘蛇,隨后出現(xiàn)的幾起案子其做,更是在濱河造成了極大的恐慌,老刑警劉巖赁还,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妖泄,死亡現(xiàn)場離奇詭異,居然都是意外死亡艘策,警方通過查閱死者的電腦和手機(jī)蹈胡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人罚渐,你說我怎么就攤上這事却汉。” “怎么了荷并?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵病涨,是天一觀的道長。 經(jīng)常有香客問我璧坟,道長既穆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任雀鹃,我火速辦了婚禮幻工,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘黎茎。我一直安慰自己囊颅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布傅瞻。 她就那樣靜靜地躺著踢代,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嗅骄。 梳的紋絲不亂的頭發(fā)上胳挎,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音溺森,去河邊找鬼慕爬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛屏积,可吹牛的內(nèi)容都是我干的医窿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼炊林,長吁一口氣:“原來是場噩夢啊……” “哼姥卢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起渣聚,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤独榴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后饵逐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體括眠,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年倍权,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡薄声,死狀恐怖当船,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情默辨,我是刑警寧澤德频,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站缩幸,受9級特大地震影響壹置,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜表谊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一钞护、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爆办,春花似錦难咕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跨算,卻和暖如春爆土,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诸蚕。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工雾消, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人挫望。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓立润,卻偏偏與公主長得像,于是被迫代替她去往敵國和親媳板。 傳聞我的和親對象是個(gè)殘疾皇子桑腮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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