在Android中,JSBridge已经不是什么新鲜的事物了,各家的实现方式也略有差异。大多数人都知道WebView存在一个漏洞,见WebView中接口隐患与手机挂马利用,虽然该漏洞已经在Android 4.2上修复了,即使用@JavascriptInterface代替addJavascriptInterface,但是由于兼容性和安全性问题,基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现,所以我们只能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本的方案。

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

那么,怎么去实现一个兼容Android各版本又具有一定安全性的JSBridge呢?我们知道,在WebView中,如果java要调用js的方法,是非常容易做到的,使用WebView.loadUrl(“javascript:function()”)即可,这样,就做到了JSBridge的native层调用h5层的单向通信,但是h5层如何调native层呢,我们需要寻找这么一个通道,仔细回忆一下,WebView有一个方法,叫setWebChromeClient,可以设置WebChromeClient对象,而这个对象中有三个方法,分别是onJsAlert,onJsConfirm,onJsPrompt,当js调用window对象的对应的方法,即window.alertwindow.confirmwindow.prompt,WebChromeClient对象中的三个方法对应的就会被触发,我们是不是可以利用这个机制,自己做一些处理呢?答案是肯定的。

至于js这三个方法的区别,可以详见w3c JavaScript 消息框 。一般来说,我们是不会使用onJsAlert的,为什么呢?因为js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响,而confirm和prompt的使用频率相对alert来说,则更低一点。那么到底是选择confirm还是prompt呢,其实confirm的使用频率也是不低的,比如你点一个链接下载一个文件,这时候如果需要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,类似这种场景还是很多的,因此不能占用confirm。而prompt则不一样,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,所以我们完全可以使用这个方法。该方法就是弹出一个输入框,然后让你输入,输入完成后返回输入框中的内容。因此,占用prompt是再完美不过了。

到这一步,我们已经找到了JSBridge双向通信的一个通道了,接下来就是如何实现的问题了。本文中实现的只是一个简单的demo,如果要在生产环境下使用,还需要自己做一层封装。

要进行正常的通信,通信协议的制定是必不可少的。我们回想一下熟悉的http请求url的组成部分。形如http://host:port/path?param=value,我们参考http,制定JSBridge的组成部分,我们的JSBridge需要传递给native什么信息,native层才能完成对应的功能,然后将结果返回呢?显而易见我们native层要完成某个功能就需要调用某个类的某个方法,我们需要将这个类名和方法名传递过去,此外,还需要方法调用所需的参数,为了通信方便,native方法所需的参数我们规定为json对象,我们在js中传递这个json对象过去,native层拿到这个对象再进行解析即可。为了区别于http协议,我们的jsbridge使用jsbridge协议,为了简单起见,问号后面不适用键值对,我们直接跟上我们的json字符串,于是就有了形如下面的这个uri

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)

该方法的实现其实很简单,从一个Map中查找key是不是存在,不存在则反射拿到对应的Class中的所有方法,将方法是public static void 类型的,并且参数是三个参数,分别是Webview,JSONObject,Callback类型的,如果满足条件,则将所有满足条件的方法put进去,整个实现如下

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 HashMap getAllMethod(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] == 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()方法将返回数据传入即可,

 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);                }            });        }    }}" data-snippet-id="ext.5fc9ec3243b9e8d49c59bbae9af4abb3" data-snippet-saved="false" data-codota-status="done">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回调,虽然这个回调没有什么意义。

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,输入

<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进来测试了

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

效果如下图所示
Android JSBridge的原理与实现_第1张图片

可以看到整个过程都走通了,然后我们测试下子线程回调,在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显示

Android JSBridge的原理与实现_第2张图片

很完美,代码也不多,就实现了功能。如果你需要使用到生成环境中去,上面的代码你一定要再自己封装一下,因为我只是简单的实现了功能,其他因素并没有考虑太多。

当然你也可以参考一个开源的实现
Safe Java-JS WebView Bridge

最后还是惯例,贴上代码

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

更多相关文章

  1. android sdk manager安装SDK出现错误解决方法
  2. Android SDK无法更新解决方法
  3. Android自动化测试之虚拟机中软件安装方法(四)
  4. Android 调用第三方so中方法记录
  5. Android图像格式类及图像转换方法
  6. 不同Android版本设备正确获取屏幕分辨率的通用方法

随机推荐

  1. android studio 2.0 按钮点击切换样式示
  2. Android 主题theme说明 摘记
  3. 【Android Studio】导入外部jar包
  4. ListView中嵌入button,点击监听无响应的解
  5. ubuntu下android studio开发环境搭建
  6. Android Studio第四十期 - 上传头像功能
  7. Android如何愉快的使用Spinner列表选择框
  8. Android 应用程序如何获取system权限
  9. Android中默认壁纸的修改
  10. 前端混合开发总结