Android JSBridge的原理与实现

在Android中。JSBridge已经不是什么新奇的事物了,各家的实现方式也略有差别。javascript

大多数人都知道WebView存在一个漏洞。见WebView中接口隐患与手机挂马利用,尽管该漏洞已经在Android 4.2上修复了,即便用@JavascriptInterface取代addJavascriptInterface,但是因为兼容性和安全性问题,基本上咱们不会再利用Android系统为咱们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现。因此咱们仅仅能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本号的方案。php

首先咱们来了解一下为何要使用JSBridge,在开发中。为了追求开发的效率以及移植的便利性,一些展现性强的页面咱们会偏向于使用h5来完毕。功能性强的页面咱们会偏向于使用native来完毕。而一旦使用了h5,为了在h5中尽量的获得native的体验,咱们native层需要暴露一些方法给js调用,比方,弹Toast提醒。弹Dialog,分享等等,有时候甚至把h5的网络请求放着native去完毕,而JSBridge作得好的一个典型就是微信。微信给开发人员提供了JSSDK,该SDK中暴露了很是多微信native层的方法,比方支付,定位等。css

那么。怎么去实现一个兼容Android各版本号又具备必定安全性的JSBridge呢?咱们知道。在WebView中,假设java要调用js的方法。是很是easy作到的,使用WebView.loadUrl(“javascript:function()”)就能够,这样。就作到了JSBridge的native层调用h5层的单向通讯,但是h5层怎样调native层呢,咱们需要寻找这么一个通道。细致回顾一下,WebView有一个方法,叫setWebChromeClient,可以设置WebChromeClient对象,而这个对象中有三个方法。各自是onJsAlert,onJsConfirm,onJsPrompt。当js调用window对象的相应的方法,即window.alertwindow.confirmwindow.prompt,WebChromeClient对象中的三个方法相应的就会被触发,咱们是否是可以利用这个机制,本身作一些处理呢?答案是确定的。html

至于js这三个方法的差异,可以详见w3c JavaScript 消息框 。通常来讲,咱们是不会使用onJsAlert的,为何呢?因为js中alert使用的频率仍是很是高的,一旦咱们占用了这个通道,alert的正常使用就会受到影响。而confirm和prompt的使用频率相对alert来讲,则更低一点。那么到底是选择confirm仍是prompt呢,事实上confirm的使用频率也是不低的,比方你点一个连接下载一个文件,这时候假设需要弹出一个提示进行确认。点击确认就会下载。点取消便不会下载,类似这种场景仍是很是多的,所以不能占用confirm。而prompt则不同,在Android中。差点儿不会使用到这种方法,就是用。也会进行本身定义。因此咱们全然可以使用这种方法。该方法就是弹出一个输入框。而后让你输入,输入完毕后返回输入框中的内容。所以。占用prompt是再完美只是了。java

到这一步,咱们已经找到了JSBridge双向通讯的一个通道了。接下来就是怎样实现的问题了。本文中实现的仅仅是一个简单的demo,假设要在生产环境下使用。还需要本身作一层封装。android

要进行正常的通讯,通讯协议的制定是不可缺乏的。git

咱们回忆一下熟悉的http请求url的组成部分。github

形如http://host:port/path?param=value。咱们參考http,制定JSBridge的组成部分,咱们的JSBridge需要传递给native什么信息,native层才干完毕相应的功能,而后将结果返回呢?显而易见咱们native层要完毕某个功能就需要调用某个类的某个方法,咱们需要将这个类名和方法名传递过去。此外,还需要方法调用所需的參数,为了通讯方便。native方法所需的參数咱们规定为json对象。咱们在js中传递这个json对象过去。native层拿到这个对象再进行解析就能够。为了差异于http协议,咱们的jsbridge使用jsbridge协议,为了简单起见,问号后面不适用键值对。咱们直接跟上咱们的json字符串,因而就有了形如如下的这个uriweb

json

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,进行回调。

因而,完整的协议定义例如如下:

jsbridge://className:callbackAddress/methodName?

jsonObj

假设咱们需要调用native层的Logger类的log方法。固然这个类以及方法确定是遵循某种规范的,不是所有的java类都可以调用。否则就跟文章开头的WebView漏洞同样了,參数是msg。运行完毕后js层要有一个回调。那么地址就例如如下

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中。如下是两个样例。一个成功调用,一个调用失败。

{
    "code":500,
    "msg":"method is not exist",
    "result":null }
{
    "code":0,
    "msg":"ok",
    "result":{ "key1":"returnValue1", "key2":"returnValue2", "key3":{ "nestedKey":"nestedValue" "nestedArray":["value1","value2"] } } }

那么这个结果怎样返回呢。native调用js暴露的方法就能够。而后将js层传给native层的port一并带上,进行调用就能够,调用的方式就是经过WebView.loadUrl方式来完毕,例如如下。

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

关于JsBridge.onFinish方法的实现。后面再叙述。前面咱们提到了native层的方法必须遵循某种规范。否则就很是不安全了。在native中,咱们需要一个JSBridge统一管理这些暴露给js的类和方法,并且能实时增长,这时候就需要这么一个方法

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

这个javaClass就是知足某种规范的类,该类中有知足规范的方法,咱们规定这个类需要实现一个空接口,为何呢?主要做用就混淆的时候不会错误发生,另外一个做用就是约束JSBridge.register方法第二个參数必须是该接口的实现类。那么咱们定义这个接口

public interface IBridge{
}

类规定好了。类中的方法咱们还需要规定,为了调用方便,咱们规定类中的方法必须是static的,这样直接依据类而没必要新建对象进行调用了(还要是public的)。而后该方法不具备返回值,因为返回值咱们在回调中返回,既然有回调,參数列表就确定有一个callback。除了callback,固然还有前文提到的js传来的方法调用所需的參数,是一个json对象,在java层中咱们定义成JSONObject对象;方法的运行结果需要经过callback传递回去。而java运行js方法需要一个WebView对象。因而,知足某种规范的方法原型就出来了。

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

}

js层除了上文说到的JSBridge.onFinish(port,jsonObj);方法用于回调。应该另外一个方法提供调用native方法的功能,该函数的原型例如如下

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

在call方法中再将參数组合成形如如下这个格式的uri

jsbridge://className:callbackAddress/methodName?jsonObj

而后调用window.prompt方法将uri传递过去,这时候java层就会收到这个uri,再进一步解析就能够。

万事具有了,仅仅欠怎样编码了,别急,如下咱们一步一步的来实现,先完毕js的两个方法。新建一个文件,命名为JSBridge.js

(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的类和方法,同一时候也便于混淆

public interface IBridge {
}

首先咱们要将js传来的uri获取到,编写一个WebChromeClient子类。

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

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的类和方法

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

该方法的实现事实上很是easy,从一个Map中查找key是否是存在,不存在则反射拿到相应的Class中的所有方法。将方法是public static void 类型的。并且參数是三个參数,各自是Webview,JSONObject。Callback类型的,假设知足条件。则将所有知足条件的方法put进去,整个实现例如如下

public class JSBridge {
    private static Map<String, HashMap<String, Method>> 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 HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
        HashMap<String, Method> 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] == Callback.class) {
                    mMethodsMap.put(name, method);
                }
            }
        }
        return mMethodsMap;
    }
}

而至于JSBridge类中的callJava方法,就是将js传来的uri进行解析,而后依据调用的类名别名从刚刚的map中查找是否是存在。存在的话拿到该类所有方法的methodMap。而后依据方法名从methodMap拿到方法,反射调用。并将參数传进去。參数就是前文说的知足条件的三个參数,即WebView,JSONObject。Callback。

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)) {
            HashMap<String, 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()方法将返回数据传入就能够,

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<WebView> 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回调。尽管这个回调没有什么意义。

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且參数列表知足条件,这样才干找到该方法。

不要忘记将该类注冊进去

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

进行一下简单的測试,将以前实现好的JSBridge.js文件扔到assets文件夹下,而后新建index.html。输入

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>JSBridge</title>
    <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>

很是easy,就是按钮点击时调用JSBridge.call()方法,回调函数是alert出返回的结果。

接着就是使用WebView将该index.html文件load进来測试了

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

效果例如如下图所看到的
这里写图片描写叙述

可以看到整个过程都走通了,而后咱们測试下子线程回调,在BridgeImpl中增长測试方法

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中增长

<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

相关文章
相关标签/搜索