通过Prompt实现JSBridge

来源:互联网 发布:python coroutine 编辑:程序博客网 时间:2024/06/09 16:34

一直比较好奇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页面的<p></p>标签中。


1.首先在Html中我们需要一个点击事件的function。可以大致定义为:

var toast=function (){

JSBridge.NativeCall('CPToast','toast',{ 'params' : ' helloword ' },function(res){
         alert(JSON.stringify(res));
         document.getElementById("toast_div").innerHTML='<p>'+res.data+'</p>';
         });

}



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<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;
}

备注: exposedMethods这个集合是一个至关重要的集合,他是整个JSBridge中Handler的集合,我们经常会看到一些方法registerHandler(handlerName , cls <? extends class>)

我们通常会好奇这个注册进去的handler会跑去哪里?其实,所有我们注册的handler都会被保存到exposedMethods这个集合中,这个集合的定义如下:

private static HashMap<String,HashMap<String, Method>> exposedMethods=new HashMap<String, HashMap<String,Method>>();

我们可以看到,其实这个集合实际上是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

原创粉丝点击