一直比较好奇JSBridge到底是个什么,正好这段时间有空,就研究了一下。其实实现JSBridge可以通过很多种方式,包括alert,confirm,prompt以及url拦截等等,我们甚至还能,使用addJavaScriptInterface接口实现,但是这个接口在Android4.2之前暴露了一个远程挂马问题,虽然google紧急抢修,但是一朝被蛇咬,十年怕井绳,我们几乎还是没有用今这个addJavaScriptInterface接口实现Native和JS通信,今天主要说一说通过Prompt方法实现JSBridge。
要了解JSBridge,首先要明白JSBridge是什么,有什么作用。
JSBridge可以理解为一个通信桥,连接通信双方,或者说时一个中转基站,可以将一方的信息送到另一方,并将另一方的信息返还给发送方。我们大可以将其理解为一个Native和JS的通信工具,或者说一种通信协议。有了JSBridge,我们在Native和JS之间的通信就方便多了。
我么知道,在报文收发的时候是遵守特定协议的,例如http协议,如果不遵守这个协议,那么一方发送的报文在另一方就要出问题,或者根本就到不了另一方。JSBridge也一样,要实现它,我们也需要定制自己的协议,例如 JSBridge:// className : port / method ? params 这看上去是不是很像http://www.baidu.com:80/address.do ? key1=value1
其实JSBridge就是基于这种原理实现的,我们在实现JSBridge时,需要和客户端定制一个类似于这样的协议JSBridge:// className : port / method ? params,在JS中我们每次需要跟Native通信时,只需要使用window.prompt(uri, "");就能够调起客户端在WebChromeClient子类中实现的onJsPrompt方法。在这个方法中,我么针对这段协议进行解析,分别拿到JSBridge,className ,port ,method ,params 这些数据。
JSBridge:协议头,我们可以根据这个协议头判断这是不是我们自己的协议。
className : 指向某各类的handler名,例如JS希望Native执行一下Toast(吐司),那Native这边需要提供一个类,并且这个类中要有一个方法来实现Toast,并且在初始化时需要为该类提供一个handlerName。
method : 方法名,即className类中的某个方法的名字。
params : 参数,例如JS希望Native来Toast指定的内容helloword,那么我们希望JS吧helloword以参数的形式带到Native。通常params是一串JSON格式的字符串
port : 回调方法的下标,可以理解为http协议中的端口号。通常JS通知Native做完某件事后,JS希望得到Native做完这件事的返回结果,但是JS又不会把自己的回调function传到Native端,JS只是在自己那里维护了一个回调数组,每次将回调方法丢到这个数组里面,并将其下标传到Native,等到Native执行完后,只需要将执行结果和这个port回传给JS,那么JS就能通过这个port在回调数组中找到对应的回调函数,并执行该函数。
协议分析完了,我们接下来看看具体的实现。
假设我现在定义了一个协议: JSBridge:// HttpRequest : 953215 / post ? { 'params':{ 'key1' : 'value1' } , 'url' : 'https://100.100.40.120/portal/address.do' }
其实从协议中我们可以看出我们要实现的功能大致是: JS希望发起https://100.100.40.120/portal/address.do请求,并且请求参数是:{ 'key1' : 'value1' },但是做过混合开发的都知道,一般这些请求我们都通过Native来转发,所以JS实际上会把请求的地址url和参数params传到Native,让Native来实现这个请求。那么JS会把这些东西传到哪里去?毋庸置疑,首先这些东西会最先进入到WebChromeClient子类中实现的onJsPrompt方法中,由这个方法来处理后,最终会将参数传给HttpRequest 类中的一个方法post方法中去。然后由这个post方法去发送请求,并得到请求结果,然后将请求结果组装成特定格式的result(一般是JSON格式),然后将这个result和先前拿到的port一起传给JS的一个自定义的function CallFinish(result,port){ }方法中,在这个方法中,JS通过port在回调数组中找到相关的回调方法,并执行回调。
说完大致思路,接下来要说具体实现了。就拿一次Toast来说,假设我们现在希望点击Html中的按钮,实现让原生弹出内容“helloword”,并且向JS返回结果“success”并显示到Html页面的
标签中。1.首先在Html中我们需要一个点击事件的function。可以大致定义为:
var toast=function (){
JSBridge.NativeCall('CPToast','toast',{ 'params' : ' helloword ' },function(res){ '+res.data+'
alert(JSON.stringify(res));
document.getElementById("toast_div").innerHTML='
});
}
2.咱们需要在JSBridge.js中实 现NativeCall这个方法,得到特殊格式的url,并调用 windows.prompt(url , '')方法,将值传到原生,部分代码如下。
NativeCall: function (obj, method, params, callback) {
var port = Util.getPort();
this.callbacks[port] = callback;
var uri=Util.getUri(obj, method, params,port);
window.prompt(uri, "");
}
getPort: function () {
//创造一个非常大范围的随机数 1<<30 = 1 073 741 824 为保证port的唯一性,10亿的重复概率非常小
return Math.floor(Math.random() * (1 << 30));
}
getParam:function(obj){
if (obj && typeof obj === 'object') {
return JSON.stringify(obj);
} else {
return obj || '';
}
}
getUri:function(obj, method, params, port){
params = this.getParam(params);
var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;
return uri;
},
备注:JSBRIDGE_PROTOCOL实际上就是我们定义的协议头JSBridge, this.callbacks[port] = callback;实际上就是将回调放进数组指定下标位置
3.在JS执行完windows.prompt方法之后,将会走到Native的WebChromeClient的子类中的方法去。
public class CGWebChromeClient extends WebChromeClient{
@Override
public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result) {
result.confirm(CGJSBridge.callJava(view, message));
// Toast.makeText(view.getContext(),"message="+message,Toast.LENGTH_LONG).show();
return true;
}
}
实际上在这里看不出来处理了什么,其实他的处理都放在了CGJSBridge类的callJava方法中。该方法先判断了url的合法性,然后去解析url得到类名和方法名,然后通过Method类的invoke方法执行解析出来的指定方法名的方法。calljava方法如下:
public static String callJava(WebView webView, String uriString) {
// TODO Auto-generated method stub
String methodName = "";
String className = "";
String param = "{}";
String port = "";
//判断协议头合法性
if (!TextUtils.isEmpty(uriString) && uriString.startsWith(GlobalParams.PROTOCAL_HEAD)) {
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
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;
}
备注: exposedMethods这个集合是一个至关重要的集合,他是整个JSBridge中Handler的集合,我们经常会看到一些方法registerHandler(handlerName , cls )
我们通常会好奇这个注册进去的handler会跑去哪里?其实,所有我们注册的handler都会被保存到exposedMethods这个集合中,这个集合的定义如下:
private static HashMap
我们可以看到,其实这个集合实际上是Map嵌套Map,外层的Map的键名其实就是handlerName,其对应的值又是一个Map,这个Map是我们传进来的类cls的所有方法的集合。
实际上我们callJava中做了两件事,第一,判断协议的合法性,解析协议;第二,判断exposedMethods集合中有没有JS那边指定handler名,实际上我们是在Html的方法中指定了handlerName的
这个handlerName是CPToast,可以返回去看。如果handlerName是存在的,则去找到这个handlerName指向的某个类的方法集合,然后匹配方法名,调用到对应的方法。
至于调用到方法后怎么toast应该不用说吧,学过一点android都应该会的。
4.在调用指定类的方法后,我们需要将服务端返回的结果回传给JS,那么怎么回传呢?
实际上JS上是定义了一个通用的提供给Native调用的方法,其方法如下:
onFinish: function (port, jsonObj){
var callback = this.callbacks[port];
callback && callback(jsonObj);
delete this.callbacks[port];
}
那么我们Native怎么调用到这个方法呢,其实很容易:其实我们只需要在loadurl中走一段这样的代码就行了: javascript:JSBridge.onFinish('%s', %s);我们也可以把这串东西封装成一个方法丢到CallBack类中
public void apply(JSONObject jsonObject) {
final String execJs = String.format(GlobalParams.CallJS_FORMAT, mPort, String.valueOf(jsonObject));
if (mWebViewRef != null && mWebViewRef.get() != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mWebViewRef.get().loadUrl(execJs);
}
});
}
}
这个JSBridge其实是一个windows对象,它里面有个方法onFinish。
至于怎样让JSBridge更安全,可以采用一些加密手段加密数据,这里就不详细说明了。
那么至此怎么流程就结束了,哎呀妈,手打是真累。如果想看更详细的完整代码,可以去下载Demo,我这边就不一一粘贴了。
下载地址:http://download.csdn.net/detail/xiangxiang_8_8/9885531