到这一步,我们已经找到了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传来的uri获取到,编写一个WebChromeClient子类。
public class JSBridgeWebChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, StringdefaultValue, 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进去,整个实现如下
而至于JSBridge类中的callJava方法,就是将js传来的uri进行解析,然后根据调用的类名别名从刚刚的map中查找是不是存在,存在的话拿到该类所有方法的methodMap,然后根据方法名从methodMap拿到方法,反射调用,并将参数传进去,参数就是前文说的满足条件的三个参数,即WebView,JSONObject,Callback。
看到该方法中使用了 new Callback(webView, port)进行新建对象,该对象就是用来回调js中回调方法的java对应的类。这个类你需要将js传来的port传进来之外,还需要将WebView的引用传进来,因为要使用到WebView的loadUrl方法,为了防止内存泄露,这里使用弱引用。如果你需要回调js的callback,在对应的方法里调用一下callback.apply()方法将返回数据传入即可,
唯一需要注意的是apply方法我把它扔在主线程执行了,为什么呢,因为暴露给js的方法可能会在子线程中调用这个callback,这样的话就会报错,所以我在方法内部将其切回主线程。
编码完成的差不多了,那么就剩实现IBridge即可了,我们来个简单的,就来显示Toast为例好了,显示完给js回调,虽然这个回调没有什么意义。
你可以往该类中扔你需要的方法,但是必须是public static void且参数列表满足条件,这样才能找到该方法。
不要忘记将该类注册进去
JSBridge.register("bridge", BridgeImpl.class);
进行一下简单的测试,将之前实现好的JSBridge.js文件扔到assets目录下,然后新建index.html,输入
很简单,就是按钮点击时调用JSBridge.call()方法,回调函数是alert出返回的结果。
接着就是使用WebView将该index.html文件load进来测试了
mWebView.loadUrl("file:///android_asset/index.html");
效果如下图所示
可以看到整个过程都走通了,然后我们测试下子线程回调,在BridgeImpl中加入测试方法
在index.html中加入
理想的效果应该是3秒钟之后回调弹出alert显示
很完美,代码也不多,就实现了功能。如果你需要使用到生成环境中去,上面的代码你一定要再自己封装一下,因为我只是简单的实现了功能,其他因素并没有考虑太多。