jsBridge原理解析
导语
现在大多数App与H5的交互越来越多,jsBridge是一个能使webView和js交互的通信方式,本文只对https://github.com/lzyzsd/JsBridge(以下涉及到的jsBridge源码都是出自这个框架)进行分析,只要你懂得了其中的原理,你也可以封装一个jsBridge。不过在介绍jsBridge的原理前,我会简单介绍下原始的webView与js交互以及为什么要用jsBridge。
一、WebView与js交互
原始的js交互非常简单容易理解,直接给出一段客户端的代码。
//开启支持js交互
mWebView.getSettings().setJavaScriptEnabled(true);
//添加js回调接口,第一个参数是我们本地写的一个专门提供方法给H5的js对象;第二个参数是双方规定好的命名,只有注册的名称和H5那边对应才可交互。
mWebView.addJavascriptInterface(new JSRequest(), "jsRequest");
class JSRequest{
@JavascriptInterface //只有加了这个注解的方法才能被h5调用
public void actionFromH5(){
Log.v("JSRequest","H5调用了该方法");
}
}
// 本地调用H5的方法用loadUrl实现,actionFromNative是在H5里实现的一个方法
mWebView.loadUrl("javascript:actionFromNative()");
二、WebView的js对象注入漏洞
webView的js对象注入的方式非常简单,可是为什么建议使用jsBridge呢?因为该方式存在安全隐患。上述提到本地方法加了@JavascriptInterface注解才能被h5调用,这个是在Android4.2之后加的,是为了避免恶意js代码获取本地信息,如SD卡中的用户信息。但是@JavascriptInterface无法兼容4.2以前的版本,所以4.2之前的系统都有被随时侵入获取信息的可能。
那么js是如何做到的?答案是反射。4.2之前没有加@JavascriptInterface的情况下,js是可以通过你注入的js对象(addJavascriptInterface的第一个参数)直接拿到getClass(这个方法是基类Object的方法),然后再拿到Runtime对象用来执行一些命令。原理大概就是这样,如果想具体了解如何实现的,请阅读WebView的Js对象注入漏洞解决方案。
三、jsBridge源码分析
jsBridge的最大作用就是解决了WebView的安全隐患,任何版本的系统都是适用的。还是一样,下面先介绍下jsBridge的用法,一些配置我就不介绍了,直接拿主干部分。
//一些初始化代码就不展示了
······································
// 第一个参数在本地注册一个叫"submitFromWeb"的方法供H5调用,
// 第二个参数是实现了BridgeHandler接口的匿名类用来回调。
webView.registerHandler("submitFromWeb", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
// 这里的data是H5传给本地的数据,function.onCallBack是回调给H5的字符串数据
Log.i(TAG, "handler = submitFromWeb, data from web = " + data);
function.onCallBack("submitFromWeb exe, response data 中文 from Java");
}
});
// 第一个参数是H5页面注册的一个名为"functionInJs"的方法
// 第二个参数是客户端本地传给H5的字符串
// 第三个参数是实现回调接口的匿名内部类
webView.callHandler("functionInJs", "data from Java", new CallBackFunction() {
@Override
public void onCallBack(String data) {
// TODO Auto-generated method stub
// data是H5返回给客户端的数据
Log.i(TAG, "reponse data from js " + data);
}
});
3.1 H5调客户端
jsBridge的源码是很少的,理解起来不是那么困难,只要一步步往下走就好了,首先我们从registerHandler出发:
// BridgeWebView.java
public void registerHandler(String handlerName, BridgeHandler handler) {
if (handler != null) {
messageHandlers.put(handlerName, handler);// 每个回调接口都对应一个key值(也就是你命名的方法名)
}
}
registerHandler方法就是这么简单,客户端操作已经到此结束了。我认为jsBridge最神奇的地方就是WebViewJavaScriptBridge.js这个js文件,对于不熟悉H5开发的同学可能有点看不懂(包括我),但是其实这个js文件的内容和BridgeWebView.java非常类似,大概看懂几个重要方法的作用即可。下面是一段H5调用客户端方法的代码:
// demo.html
// testClick1方法是H5页面点击某个按钮触发的,然后会调客户端的方法。
function testClick1() {
// call native method
// 第一个参数是客户端命名的方法
// 第二个参数是传给客户端的数据
// 第三个参数是客户端返回数据给H5的回调方法
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': '中文测试'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
}
但是这个callHandler方法不是H5写的,而是客户端本地的WebViewJavaScriptBridge.js文件里的方法,这个文件里的内容是直接可以注入到H5页面(不得不感叹H5的方便之处)。
// WebViewJavaScriptBridge.js
// 提供给H5的js方法
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
// 对应上面方法里的_doSend,在发送消息队列中加入消息,触发native请求
function _doSend(message, responseCallback) {
// responseCallback按命名理解就是响应回调,也就是说是客户端再传数据给H5的时候用到的
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
// 我的理解是这行代码会触发WebViewClient中的shouldOverrideUrlLoading,这是交互的关键点
// 返回给客户端的url是"yy://__QUEUE_MESSAGE__/"
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
上面的注释已经写明H5最终会触发WebViewClient中的shouldOverrideUrlLoading:
// BridgeWebViewClient.java
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // url开头是否是"yy://return/_fetchQueue/",说明是H5要返回数据给客户端了
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //url开头是否是"yy://__QUEUE_MESSAGE__/",说明H5要调用客户端了。
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
上面的代码已经走到H5调用客户端了,接下去跟进webView.flushMessageQueue()看看:
// BridgeWebView.java
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
// 先不要看这里,因为代码还没走到这一步,等回调的时候才会走这里,下面会有提示再来看。(省略部分代码)
List<Message> list = null;
try {
list = Message.toArrayList(data);// 解析H5传过来的Json数据
} catch (Exception e) {
e.printStackTrace();
return;
}
for (int i = 0; i < list.size(); i++) {
Message m = list.get(i);
String responseId = m.getResponseId();
// 如果是客户端调用H5方法则会有responseId这个值,也就是webView.callHandler
if (!TextUtils.isEmpty(responseId)) {
CallBackFunction function = responseCallbacks.get(responseId);
String responseData = m.getResponseData();
function.onCallBack(responseData);// 回调到webView.callHandler里面的回调方法
responseCallbacks.remove(responseId);
} else {// H5调用客户端会走这里
CallBackFunction responseFunction = null;
final String callbackId = m.getCallbackId();// 一般情况下都是有callbackId的,这是H5那边设置的
if (!TextUtils.isEmpty(callbackId)) {
// 这里实现的回调接口是提供给客户端再次去和H5交互的机会,对应webView.registerHandler(name,handler)里面的function
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);// js传过来的callbackId赋值给responseId回传给js,这样就可以配对了。
responseMsg.setResponseData(data);
queueMessage(responseMsg);// 向H5发送消息
}
};
}
BridgeHandler handler;
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
}
if (handler != null){// 客户端只有registerHandler后取出来的handler才不为null
// 这一步就是调到了webView.registerHandler(name,handler)第二个参数BridgeHandler里了
handler.handler(m.getData(), responseFunction);
}
}
}
}
}
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl); // 加载jsUrl="javascript:WebViewJavascriptBridge._fetchQueue();"
// 键值对形式存放响应回调接口,这里的key是"_fetchQueue"
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
现在整理下发现H5第一次调客户端时只是实现一个回调方法(当然这个回调方法非常重要),然后用键值对的方式存储之后供下次配对。客户端会再一次loadUrl加载本地js文件中的_fetchQueue()方法:
// WebViewJavaScriptBridge.js
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
// 会触发客户端的shouldOverrideUrlLoading,传递url的形式是:"yy://return/_fetchQueue/"+H5给客户端的数据
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
shouldOverrideUrlLoading的代码已经在之前贴出过,这里就不再贴出,然后会调用webView.handlerReturnData(url):
void handlerReturnData(String url) {
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);// 拿到functionName="_fetchQueue"
CallBackFunction f = responseCallbacks.get(functionName);// 拿到的key就是为配对键值对的啊!还记得上面存储过了吗?
String data = BridgeUtil.getDataFromReturnUrl(url);// 拿到H5给客户端的数据
if (f != null) {
f.onCallBack(data);// 回调
responseCallbacks.remove(functionName);
return;
}
}
f.onCallBack(data)就是在flushMessageQueue实现的那个回调方法啊,所以这个时候就要回去看看那个方法里面具体做了什么(重点已注释),到此为止H5调客户端的方法流程基本已经走完,queueMessage(responseMsg)方法就不再具体讲了,作用就是向H5发消息(类似于客户端调用H5方法,但是有区别)。
3.2 客户端调用H5方法
我觉得再从源码一步步讲解是没什么意义的,只要理解了H5调用客户端方法就可以了,因为流程和H5调用客户端方法是相反的,也就是说WebViewJavaScriptBridge.js和BridgeWebView.java是功能相似的不同语言所写的文件,接下来我通过一张流程图过一遍客户端调用H5方法 :

总结
现在的App开发熟练使用WebView以及和js交互是很有必要的,jsBridge的实现也不复杂,只要和H5定好协议,完全可以自己写一个jsBridge通信方式的框架。而且多阅读源码有助于自己的提升,从这些简单而精妙的源码入手是再合适不过了。