在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的方法。是非常easy做到的,使用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)

该方法的实现事实上非常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。

 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;
}" data-snippet-id="ext.7245ba2e634a08805a3436f575725685" data-snippet-saved="false" data-codota-status="done">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<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。输入


    JSBridge

JSBridge 測试

  • 測试showToast
" data-snippet-id="ext.72aeb753849e78157e5829319fd466ef" data-snippet-saved="false" data-codota-status="done"><!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中增加

  • 測试子线程回调
  • " data-snippet-id="ext.b1809e021627d8303574ede18b360951" data-snippet-saved="false" data-codota-status="done"><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

    最新文章

    1. 如何自动化一键部署PHP项目
    2. javascript event(事件对象)详解
    3. 深入理解Java虚拟机之读书笔记三 内存分配策略
    4. Oracle 触发器的简单命令
    5. leetcode 374
    6. SQL null值 查询null
    7. Docker CPU 资源限制——CPU分片功能测试
    8. GPS定位原理
    9. PDF 补丁丁 0.4.1.688 测试版发布(请务必用其替换 682 测试版)
    10. 【Mac】Mac键盘实现Home, End, Page UP, Page DOWN
    11. Abstract Factory
    12. BI事实上的和维表定义
    13. Angularjs跳转切换至对应选项卡
    14. 201521123050 《Java程序设计》第4周学习总结
    15. testng增加失败重跑机制
    16. ubuntu创建idea桌面快捷方式
    17. Django---框架简介和工程搭建
    18. ABP框架系列之四十五:(Quartz-Integration-Quartz-集成)
    19. WEB技术路线图
    20. C# 敏捷1

    热门文章

    1. 在asp.net中使用jQuery实现类似QQ网站的图片切割效果
    2. c++ placement new概念
    3. linux文件系统命令(1)---概述
    4. Android -- 重置Bitmap大小&amp;&amp;Bitmap转角度
    5. [Node.js]31. Level 7: Redis coming for Node.js, Simple Redis Commands
    6. 微信小程序 - 使用npm(第三方包)
    7. php之快速入门学习-8(if-else)
    8. Centering window on the screen
    9. js 数组循环删除元素或对象
    10. CSS nth-child、first-child、last-child、nth-of-type、first-of-type和last-of-type选择器使用