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行业人员进入!

相关文章
相关标签/搜索