Android JSBridge的原理與實現

原文出處: 安卓弟(@_安卓弟)  javascript

在Android中,JSBridge已經不是什麼新鮮的事物了,各家的實現方式也略有差別。大多數人都知道WebView存在一個漏洞,見WebView中接口隱患與手機掛馬利用,雖然該漏洞已經在Android 4.2上修復了,即便用@JavascriptInterface代替addJavascriptInterface,可是因爲兼容性和安全性問題,基本上咱們不會再利用Android系統爲咱們提供的addJavascriptInterface方法或者@JavascriptInterface註解來實現,因此咱們只能另闢蹊徑,去尋找既安全,又能實現兼容Android各個版本的方案。html

首先咱們來了解一下爲何要使用JSBridge,在開發中,爲了追求開發的效率以及移植的便利性,一些展現性強的頁面咱們會偏向於使用h5來完成,功能性強的頁面咱們會偏向於使用native來完成,而一旦使用了h5,爲了在h5中儘量的獲得native的體驗,咱們native層須要暴露一些方法給js調用,好比,彈Toast提醒,彈Dialog,分享等等,有時候甚至把h5的網絡請求放着native去完成,而JSBridge作得好的一個典型就是微信,微信給開發者提供了JSSDK,該SDK中暴露了不少微信native層的方法,好比支付,定位等。java

那麼,怎麼去實現一個兼容Android各版本又具備必定安全性的JSBridge呢?咱們知道,在WebView中,若是java要調用js的方法,是很是容易作到的,使用WebView.loadUrl(「javascript:function()」)便可,這樣,就作到了JSBridge的native層調用h5層的單向通訊,可是h5層如何調native層呢,咱們須要尋找這麼一個通道,仔細回憶一下,WebView有一個方法,叫setWebChromeClient,能夠設置WebChromeClient對象,而這個對象中有三個方法,分別是onJsAlert,onJsConfirm,onJsPrompt,當js調用window對象的對應的方法,即window.alert,window.confirm,window.prompt,WebChromeClient對象中的三個方法對應的就會被觸發,咱們是否是能夠利用這個機制,本身作一些處理呢?答案是確定的。android

至於js這三個方法的區別,能夠詳見w3c JavaScript 消息框 。通常來講,咱們是不會使用onJsAlert的,爲何呢?由於js中alert使用的頻率仍是很是高的,一旦咱們佔用了這個通道,alert的正常使用就會受到影響,而confirm和prompt的使用頻率相對alert來講,則更低一點。那麼究竟是選擇confirm仍是prompt呢,其實confirm的使用頻率也是不低的,好比你點一個連接下載一個文件,這時候若是須要彈出一個提示進行確認,點擊確認就會下載,點取消便不會下載,相似這種場景仍是不少的,所以不能佔用confirm。而prompt則不同,在Android中,幾乎不會使用到這個方法,就是用,也會進行自定義,因此咱們徹底可使用這個方法。該方法就是彈出一個輸入框,而後讓你輸入,輸入完成後返回輸入框中的內容。所以,佔用prompt是再完美不過了。git

到這一步,咱們已經找到了JSBridge雙向通訊的一個通道了,接下來就是如何實現的問題了。本文中實現的只是一個簡單的demo,若是要在生產環境下使用,還須要本身作一層封裝。github

要進行正常的通訊,通訊協議的制定是必不可少的。咱們回想一下熟悉的http請求url的組成部分。形如http://host:port/path?param=value,咱們參考http,制定JSBridge的組成部分,咱們的JSBridge須要傳遞給native什麼信息,native層才能完成對應的功能,而後將結果返回呢?顯而易見咱們native層要完成某個功能就須要調用某個類的某個方法,咱們須要將這個類名和方法名傳遞過去,此外,還須要方法調用所需的參數,爲了通訊方便,native方法所需的參數咱們規定爲json對象,咱們在js中傳遞這個json對象過去,native層拿到這個對象再進行解析便可。爲了區別於http協議,咱們的jsbridge使用jsbridge協議,爲了簡單起見,問號後面不適用鍵值對,咱們直接跟上咱們的json字符串,因而就有了形以下面的這個uriweb

Java編程

1json

jsbridge://className:port/methodName?jsonObj數組

有人會問,這個port用來幹嗎,其實js層調用native層方法後,native須要將執行結果返回給js層,不過你會以爲經過WebChromeClient對象的onJsPrompt方法將返回值返回給js不就行了嗎,其實否則,若是這麼作,那麼這個過程就是同步的,若是native執行異步操做的話,返回值怎麼返回呢?這時候port就發揮了它應有的做用,咱們在js中調用native方法的時候,在js中註冊一個callback,而後將該callback在指定的位置上緩存起來,而後native層執行完畢對應方法後經過WebView.loadUrl調用js中的方法,回調對應的callback。那麼js怎麼知道調用哪一個callback呢?因而咱們須要將callback的一個存儲位置傳遞過去,那麼就須要native層調用js中的方法的時候將存儲位置回傳給js,js再調用對應存儲位置上的callback,進行回調。因而,完整的協議定義以下:

Java

1

jsbridge://className:callbackAddress/methodName?jsonObj

假設咱們須要調用native層的Logger類的log方法,固然這個類以及方法確定是遵循某種規範的,不是全部的java類均可以調用,否則就跟文章開頭的WebView漏洞同樣了,參數是msg,執行完成後js層要有一個回調,那麼地址就以下

Java

1

jsbridge://Logger:callbackAddress/log?{"msg":"native log"}

至於這個callback對象的地址,能夠存儲到js中的window對象中去。至於怎麼存儲,後文會慢慢倒來。

上面是js向native的通訊協議,那麼另外一方面,native向js的通訊協議也須要制定,一個必不可少的元素就是返回值,這個返回值和js的參數作法同樣,經過json對象進行傳遞,該json對象中有狀態碼code,提示信息msg,以及返回結果result,若是code爲非0,則執行過程當中發生了錯誤,錯誤信息在msg中,返回結果result爲null,若是執行成功,返回的json對象在result中。下面是兩個例子,一個成功調用,一個調用失敗。

Java

1

2

3

4

5

{

    "code":500,

    "msg":"method is not exist",

    "result":null

}

Java

1

2

3

4

5

6

7

8

9

10

11

12

{

    "code":0,

    "msg":"ok",

    "result":{

        "key1":"returnValue1",

        "key2":"returnValue2",

        "key3":{

            "nestedKey":"nestedValue"

            "nestedArray":["value1","value2"]

        }

    }

}

那麼這個結果如何返回呢,native調用js暴露的方法便可,而後將js層傳給native層的port一併帶上,進行調用便可,調用的方式就是經過WebView.loadUrl方式來完成,以下。

Java

1

mWebView.loadUrl("javascript:JSBridge.onFinish(port,jsonObj);");

關於JsBridge.onFinish方法的實現,後面再敘述。前面咱們提到了native層的方法必須遵循某種規範,否則就很是不安全了。在native中,咱們須要一個JSBridge統一管理這些暴露給js的類和方法,而且能實時添加,這時候就須要這麼一個方法

Java

1

JSBridge.register("jsName",javaClass.class)

這個javaClass就是知足某種規範的類,該類中有知足規範的方法,咱們規定這個類須要實現一個空接口,爲何呢?主要做用就混淆的時候不會發生錯誤,還有一個做用就是約束JSBridge.register方法第二個參數必須是該接口的實現類。那麼咱們定義這個接口

Java

1

2

public interface IBridge{

}

類規定好了,類中的方法咱們還須要規定,爲了調用方便,咱們規定類中的方法必須是static的,這樣直接根據類而沒必要新建對象進行調用了(還要是public的),而後該方法不具備返回值,由於返回值咱們在回調中返回,既然有回調,參數列表就確定有一個callback,除了callback,固然還有前文提到的js傳來的方法調用所需的參數,是一個json對象,在java層中咱們定義成JSONObject對象;方法的執行結果須要經過callback傳遞回去,而java執行js方法須要一個WebView對象,因而,知足某種規範的方法原型就出來了。

Java

1

2

3

public static void methodName(WebView web view,JSONObject jsonObj,Callback callback){

 

}

js層除了上文說到的JSBridge.onFinish(port,jsonObj);方法用於回調,應該還有一個方法提供調用native方法的功能,該函數的原型以下

Java

1

JSBridge.call(className,methodName,params,callback)

在call方法中再將參數組合成形以下面這個格式的uri

Java

1

jsbridge://className:callbackAddress/methodName?jsonObj

而後調用window.prompt方法將uri傳遞過去,這時候java層就會收到這個uri,再進一步解析便可。

萬事具有了,只欠如何編碼了,別急,下面咱們一步一步的來實現,先完成js的兩個方法。新建一個文件,命名爲JSBridge.js

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

(function (win) {

    var hasOwnProperty = Object.prototype.hasOwnProperty;

    var JSBridge = win.JSBridge || (win.JSBridge = {});

    var JSBRIDGE_PROTOCOL = 'JSBridge';

    var Inner = {

        callbacks: {},

        call: function (obj, method, params, callback) {

            console.log(obj+" "+method+" "+params+" "+callback);

            var port = Util.getPort();

            console.log(port);

            this.callbacks[port] = callback;

            var uri=Util.getUri(obj,method,params,port);

            console.log(uri);

            window.prompt(uri, "");

        },

        onFinish: function (port, jsonObj){

            var callback = this.callbacks[port];

            callback & callback(jsonObj);

            delete this.callbacks[port];

        },

    };

    var Util = {

        getPort: function () {

            return Math.floor(Math.random() * (1  30));

        },

        getUri:function(obj, method, params, port){

            params = this.getParam(params);

            var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;

            return uri;

        },

        getParam:function(obj){

            if (obj & typeof obj === 'object') {

                return JSON.stringify(obj);

            } else {

                return obj || '';

            }

        }

    };

    for (var key in Inner) {

        if (!hasOwnProperty.call(JSBridge, key)) {

            JSBridge[key] = Inner[key];

        }

    }

})(window);

能夠看到,咱們裏面有一個Util類,裏面有三個方法,getPort()用於隨機生成port,getParam()用於生成json字符串,getUri()用於生成native須要的協議uri,裏面主要作字符串拼接的工做,而後有一個Inner類,裏面有咱們的call和onFinish方法,在call方法中,咱們調用Util.getPort()得到了port值,而後將callback對象存儲在了callbacks中的port位置,接着調用Util.getUri()將參數傳遞過去,將返回結果賦值給uri,調用window.prompt(uri, 「」)將uri傳遞到native層。而onFinish()方法接受native回傳的port值和執行結果,根據port值從callbacks中獲得原始的callback函數,執行callback函數,以後從callbacks中刪除。最後將Inner類中的函數暴露給外部的JSBrige對象,經過一個for循環一一賦值便可。

固然這個實現是最最簡單的實現了,實際狀況要考慮的因素太多,因爲本人不是很精通js,因此只能以java的思想去寫js,沒有考慮到的因素姑且忽略吧,好比內存的回收等等機制。

這樣,js層的編碼就完成了,接下來實現java層的編碼。

上文說到java層有一個空接口來進行約束暴露給js的類和方法,同時也便於混淆

Java

1

2

public interface IBridge {

}

首先咱們要將js傳來的uri獲取到,編寫一個WebChromeClient子類。

Java

1

2

3

4

5

6

7

public class JSBridgeWebChromeClient extends WebChromeClient {

    @Override

    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {

        result.confirm(JSBridge.callJava(view, message));

        return true;

    }

}

以後不要忘記了將該對象設置給WebView

Java

1

2

3

4

5

WebView mWebView = (WebView) findViewById(R.id.webview);

WebSettings settings = mWebView.getSettings();

settings.setJavaScriptEnabled(true);

mWebView.setWebChromeClient(new JSBridgeWebChromeClient());

mWebView.loadUrl("file:///android_asset/index.html");

核心的內容來了,就是JSBridgeWebChromeClient中調用的JSBridge類的實現。前文提到該類中有這麼一個方法提供註冊暴露給js的類和方法

Java

1

JSBridge.register("jsName",javaClass.class)

該方法的實現其實很簡單,從一個Map中查找key是否是存在,不存在則反射拿到對應的Class中的全部方法,將方法是public static void 類型的,而且參數是三個參數,分別是Webview,JSONObject,Callback類型的,若是知足條件,則將全部知足條件的方法put進去,整個實現以下

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

public class JSBridge {

    private static Map> exposedMethods = new HashMap();

 

    public static void register(String exposedName, Class extends IBridge> clazz) {

        if (!exposedMethods.containsKey(exposedName)) {

            try {

                exposedMethods.put(exposedName, getAllMethod(clazz));

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

    }

 

    private static HashMapgetAllMethod(Class injectedCls) throws Exception {

        HashMap mMethodsMap = new HashMap();

        Method[] methods = injectedCls.getDeclaredMethods();

        for (Method method : methods) {

            String name;

            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {

                continue;

            }

            Class[] parameters = method.getParameterTypes();

            if (null != parameters & parameters.length == 3) {

                if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == JSCallback.class) {

                    mMethodsMap.put(name, method);

                }

            }

        }

        return mMethodsMap;

    }

}

而至於JSBridge類中的callJava方法,就是將js傳來的uri進行解析,而後根據調用的類名別名從剛剛的map中查找是否是存在,存在的話拿到該類全部方法的methodMap,而後根據方法名從methodMap拿到方法,反射調用,並將參數傳進去,參數就是前文說的知足條件的三個參數,即WebView,JSONObject,Callback。

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

public static String callJava(WebView webView, String uriString) {

        String methodName = "";

        String className = "";

        String param = "{}";

        String port = "";

        if (!TextUtils.isEmpty(uriString) & uriString.startsWith("JSBridge")) {

            Uri uri = Uri.parse(uriString);

            className = uri.getHost();

            param = uri.getQuery();

            port = uri.getPort() + "";

            String path = uri.getPath();

            if (!TextUtils.isEmpty(path)) {

                methodName = path.replace("/", "");

            }

        }

 

        if (exposedMethods.containsKey(className)) {

            HashMapString, Method> methodHashMap = exposedMethods.get(className);

 

            if (methodHashMap != null & methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {

                Method method = methodHashMap.get(methodName);

                if (method != null) {

                    try {

                        method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));

                    } catch (Exception e) {

                        e.printStackTrace();

                    }

                }

            }

        }

        return null;

    }

看到該方法中使用了 new Callback(webView, port)進行新建對象,該對象就是用來回調js中回調方法的java對應的類。這個類你須要將js傳來的port傳進來以外,還須要將WebView的引用傳進來,由於要使用到WebView的loadUrl方法,爲了防止內存泄露,這裏使用弱引用。若是你須要回調js的callback,在對應的方法裏調用一下callback.apply()方法將返回數據傳入便可,

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class Callback {

    private static Handler mHandler = new Handler(Looper.getMainLooper());

    private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";

    private String mPort;

    private WeakReference mWebViewRef;

 

    public Callback(WebView view, String port) {

        mWebViewRef = new WeakReference(view);

        mPort = port;

    }

 

    public void apply(JSONObject jsonObject) {

        final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));

        if (mWebViewRef != null & mWebViewRef.get() != null) {

            mHandler.post(new Runnable() {

                @Override

                public void run() {

                    mWebViewRef.get().loadUrl(execJs);

                }

            });

 

        }

 

    }

}

惟一須要注意的是apply方法我把它扔在主線程執行了,爲何呢,由於暴露給js的方法可能會在子線程中調用這個callback,這樣的話就會報錯,因此我在方法內部將其切回主線程。

編碼完成的差很少了,那麼就剩實現IBridge便可了,咱們來個簡單的,就來顯示Toast爲例好了,顯示完給js回調,雖然這個回調沒有什麼意義。

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public class BridgeImpl implements IBridge {

    public static void showToast(WebView webView, JSONObject param, final Callback callback) {

        String message = param.optString("msg");

        Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();

        if (null != callback) {

            try {

                JSONObject object = new JSONObject();

                object.put("key", "value");

                object.put("key1", "value1");

                callback.apply(getJSONObject(0, "ok", object));

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

    }

 

    private static JSONObject getJSONObject(int code, String msg, JSONObject result) {

        JSONObject object = new JSONObject();

        try {

            object.put("code", code);

            object.put("msg", msg);

            object.putOpt("result", result);

            return object;

        } catch (JSONException e) {

            e.printStackTrace();

        }

        return null;

    }

}

你能夠往該類中扔你須要的方法,可是必須是public static void且參數列表知足條件,這樣才能找到該方法。

不要忘記將該類註冊進去

Java

1

JSBridge.register("bridge", BridgeImpl.class);

進行一下簡單的測試,將以前實現好的JSBridge.js文件扔到assets目錄下,而後新建index.html,輸入

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

html>

head>

    meta charset="utf-8">

    title>JSBridgetitle>

    meta name="viewport"

          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"/>

    script src="file:///android_asset/JSBridge.js" type="text/javascript">script>

    script type="text/javascript">

 

    script>

    style>

 

    style>

head>

 

body>

div>

    h3>JSBridge 測試h3>

div>

ul class="list">

    li>

        div>

            button onclick="JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">

                測試showToast

            button>

        div>

    li>

    br/>

ul>

body>

html>

很簡單,就是按鈕點擊時調用JSBridge.call()方法,回調函數是alert出返回的結果。

接着就是使用WebView將該index.html文件load進來測試了

Java

1

mWebView.loadUrl("file:///android_asset/index.html");

效果以下圖所示
這裏寫圖片描述

能夠看到整個過程都走通了,而後咱們測試下子線程回調,在BridgeImpl中加入測試方法

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public static void testThread(WebView webView, JSONObject param, final Callback callback) {

        new Thread(new Runnable() {

            @Override

            public void run() {

                try {

                    Thread.sleep(3000);

                    JSONObject object = new JSONObject();

                    object.put("key", "value");

                    callback.apply(getJSONObject(0, "ok", object));

                } catch (InterruptedException e) {

                    e.printStackTrace();

                } catch (JSONException e) {

                    e.printStackTrace();

                }

            }

        }).start();

    }

在index.html中加入

Java

1

2

3

4

5

6

7

8

9

10

ul class="list">

    li>

        div>

            button onclick="JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})">

                測試子線程回調

            button>

        div>

    li>

    br/>

ul>

理想的效果應該是3秒鐘以後回調彈出alert顯示

這裏寫圖片描述

很完美,代碼也很少,就實現了功能。若是你須要使用到生成環境中去,上面的代碼你必定要再本身封裝一下,由於我只是簡單的實現了功能,其餘因素並無考慮太多。

固然你也能夠參考一個開源的實現
Safe Java-JS WebView Bridge

最後仍是慣例,貼上代碼

http://download.csdn.net/detail/sbsujjbcy/9446915

問啊-定製化IT教育平臺,牛人一對一服務,有問必答,開發編程社交頭條 官方網站:www.wenaaa.comQQ羣290551701 彙集不少互聯網精英,技術總監,架構師,項目經理!開源技術研究,歡迎業內人士,大牛及新手有志於從事IT行業人員進入!

相關文章
相關標籤/搜索