JsBridge(github地址)为混合式应用native与h5的通讯提供安全而方便的桥接。
JsBridge是通过url拦截的方式实现的。
本次源码解析分为三篇,本篇为源码分析第一篇,其他两篇请见:
android JS与Native通讯方案汇总
JsBridge源码详解(一) JS与Native通讯过程(附详细流程图)
JsBridge源码详解(二) Native与JS通讯过程(附详细流程图)
通讯的实现需要注入一段js代码,js代码的注入在页面加载完毕也就是WebViewClent的onPageFinished方法中
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(mWebView, url);
//加载本地通讯桥接的js文件
JsBridgeUtil.webViewLoadLocalJs(view, BridgeX5WebView.toLoadJs);
}
其中JsBridgeUtil的webViewLoadLocalJs方法负责注入js片段,其代码如下
public static void webViewLoadLocalJs(WebView view, String path) {
String jsContent = assetFile2Str(view.getContext(), path);
view.loadUrl("javascript:" + jsContent);
}
负责通讯的js片段以文件形式放在asserts目录下,assetFile2Str将文件以流的方式读取出来转换成字符串格式,然后通过loadUrl就加载了通讯的js片段,js的具体内容下面再一一分析。
至此,通讯的准备工作就完成了,下面来看看具体的通讯实现。
js调用native有个前提条件,就是上面讲到的js片段必须注入完毕,所以这里有部分逻辑是对该部分准备工作的监听,负责通讯的js通过发送事件的方式告诉调用方js:
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('WebViewJavascriptBridgeReady');
readyEvent.bridge = WebViewJavascriptBridge;
doc.dispatchEvent(readyEvent);
调用方js可以通过如下方式来监听该事件
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
// 准备完毕,可以通讯了
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
// 准备完毕,可以通讯了
},
false
);
}
}
监听到桥js加载完毕(步骤1),接下来就可以通讯了,调用方js通过调用桥js的callHandler函数来发起通讯(步骤2),来看一个获取H5所运行的设备的信息例子:
function getDeviceInfo(){
window.WebViewJavascriptBridge.callHandler(
'NativeHandler'
, {'funcName' : 'getDeviceInfo'}
, function(responseData) {
document.getElementById("show").innerHTML = "getDeviceInfo involved,responseData from native is " + responseData
}
);
}
各个参数的作用后面会讲到,先来看一下callHandler的实现
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
callHandler调用了内部doSend方法
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
dosend方法先是判断是否需要有结果回调,如果有则生成唯一的回调id并将该id与回调方法一起保存进map以便调用结束后通知回调方法。回调id的生成用一个自增长的整数和当前时间戳组成。唯一的id会被放进传递给native的message对象中,使得native执行完毕之后可以告诉js应该将结果返回给哪个id对应的回调方法。
需要注意的是,dosend的方法并没有直接将调用者发送来的数据告诉native,而是将message对象保存进待发送消息队列sendMessageQueue,然后改变iframe标签的src来通知native有消息要取,这是怎么做到的呢?
首先这里要简单介绍下iframe:
iframe 标签规定一个内联框架
一个内联框架被用来在当前 HTML 文档中嵌入另一个文档
iframe能在当前页面加载其他的页面,也就是说制定了其src之后会触发WebViewClient的shouldOverrideUrlLoading方法。
看一下iframe的初始化
var doc = document;
_createQueueReadyIframe(doc);
function _createQueueReadyIframe(doc) {
messagingIframe = doc.createElement('iframe');
messagingIframe.style.display = 'none';
doc.documentElement.appendChild(messagingIframe);
}
在documentElement上增加了一个不可见的iframe标签,使用该标签来触发通讯。那么究竟是如何通讯的呢?
看一下native的WebViewClient的shouldOverrideUrlLoading方法(步骤3)
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(JsBridgeUtil.YY_RETURN_DATA)) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
LogUtil.e("decode url fail:" + url, e);
}
mWebView.handlerReturnData(url);
return true;
} else if (url.startsWith(JsBridgeUtil.YY_OVERRIDE_SCHEMA)) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
LogUtil.e("decode url fail:" + url, e);
}
mWebView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
其中JsBridgeUtil.YY_RETURN_DATA 为 ”yy://return/“这部分用来处理从js端获取到的返回数据。
JsBridgeUtil.YY_OVERRIDE_SCHEMA为”yy://“ 刚刚桥js的doSend方法改变的iframe的src为CUSTOM_PROTOCOL_SCHEME + ‘: //’ + QUEUE_HAS_MESSAGE为”yy://fetchQuere“,会匹配仅第二个条件,并执行flushMessageQueue方法(步骤4)
public void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(JsBridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
// callback detail ignored
}
});
}
}
flushMessageQueue主要做了两件事,一是判断当前线程是否是主线程,只有主线程才会继续执行后面的逻辑。二是调用loadUrl方法,loadUrl方法需要两个参数,第一个是要加载url地址,第二个是一个回调,回调的内容暂且不关注,后面再说。继续看loadUrl方法
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
responseCallbacks.put(JsBridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
public static String parseFunctionName(String jsUrl) {
return jsUrl.replace("javascript:WebViewJavascriptBridge.", "").replaceAll("\\(.*\\);", "");
}
上面部分逻辑一方面执行js的调用,另一方面将回调与该js的调用关联起来。调用loadUrl的时候传入的JsBridgeUtil.JS_FETCH_QUEUE_FROM_JAVA实际为"javascript:WebViewJavascriptBridge._fetchQueue();",由此可以知道现在调用的是桥js的_fetchQueue()方法,那我们接着去分析_fetchQueue(步骤5)
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
_fetchQueue逻辑很简单,只是将前面放到messageQueue里面的消息转变成json格式并将queue置空,然后再次按照定义好的规则拼接url放置到iframe上去,这次拼接出来的内容大致是 ”yy://return/_fetchQueue/{jsonData}“,一定看上去很眼熟吧,对这次会进入到shouldOverrideUrlLoading的第一个条件并执行webview的handlerReturnData方法。(步骤6)
public void handlerReturnData(String url) {
String functionName = JsBridgeUtil.getFunctionFromReturnUrl(url);
CallBackFunction f = responseCallbacks.get(functionName);
String data = JsBridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
f.onCallBack(data);
responseCallbacks.remove(functionName);
}
}
public static String getFunctionFromReturnUrl(String url) {
String temp = url.replace(YY_RETURN_DATA, EMPTY_STR); //YY_RETURN_DATA="yy://return/"
String[] functionAndData = temp.split(SPLIT_MARK); //SPLIT_MARK="/"
if (functionAndData.length >= 1) {
return functionAndData[0];
}
return null;
}
handlerReturnData先是通过操作字符串获取方法名,在这里获取到的正是_fetchQueue ,可想而知从回调map中取出key为_fetchQueue的回调正式刚才调用fetchQueue方法时我们忽略具体实现的那个回调,现在让我们来看下这个回调到底干了什么:(步骤7)
@Override
public void onCallBack(String data) {
// deserializeMessage 反序列化消息
List list = null;
try {
list = Message.toArrayList(data);
} catch (Exception e) {
e.printStackTrace();
return;
}
if (list == null || list.size() == 0) {
return;
}
for (int i = 0; i < list.size(); i++) {
Message m = list.get(i);
String responseId = m.getResponseId();
// 是否是response CallBackFunction
if (!TextUtils.isEmpty(responseId)) {
CallBackFunction function = responseCallbacks.get(responseId);
String responseData = m.getResponseData();
function.onCallBack(responseData);
responseCallbacks.remove(responseId);
} else {
CallBackFunction responseFunction = null;
// if had callbackId 如果有回调Id
final String callbackId = m.getCallbackId();
if (!TextUtils.isEmpty(callbackId)) {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
}
};
} else {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
// do nothing
}
};
}
// BridgeHandler执行
BridgeHandler handler;
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
} else {
handler = defaultHandler;
}
if (handler != null) {
handler.handler(m.getData(), responseFunction);
}
}
}
首先将js传过来的json格式的字符串转换为message列表,如果列表不为空则挨个处理,处理过程大概如下:如果有repsponseId则说明是native调用js的返回结果,由于我们分析的是js调用native所以这条分路暂时不管,看下另外一条,如果callBackId不为空生成一个新的callBackFunction,并将callBackId在返回数据中一并返回,目的是为了方便js找到该callBackId对应的调用方js的callBackFunction,如果callBackId为空则说明这次请求不需要结果回调,则生成一个空实现的回调。这里callBackId是否为空是由最初调用方js决定的,可以参考 步骤2 处获取设备信息的例子。
接着会根据js设置的 BridgeHandler的名字去匹配对应的BridgeHandler,要调用的BridgeHandler的名字也是在最初调用方js那里确定好的,在 步骤2 处例子是 ”NativeHandler“。
那么这些BridgeHandler是在什么时候又是怎样注册进来的呢?这个问题比较简单,注册时机一般在使用webview的activity或者fragment对webview进行初始化的时候,注册方法更简单这里不浪费口舌。
假如根据handlerName能匹配到对应的BridgeHandler(一般都能匹配到,因为BridgeHandler的name都是两端提前商量好的),则执行BridgeHandler的handler方法,然后handler方法会根据事先定义好的规则匹配要执行的方法和传入的参数(如果有的话),执行完之后回调刚才根据callBackId是否为空生成的CallBackFunction (步骤8),例如:
@Override
public void handler(String data, CallBackFunction function) {
//获取js端要调用的方法名
String funcName = getFunctionName(data);
switch (funcName) {
case "getDeviceInfo":
function.onCallBack(new Gson().toJson(new DeviceInfo()));
break;
case "xxx":
...
...
default:
function.onCallBack("no such function named: " + funcName);
break;
}
}
BridgeHandler将处理结果通过执行CallBackFunction的onCallBack方法回传给js,通过步骤7中onCallBack的具体实现我们知道具体传递工作交给了queueMessage方法,queueMessage又调用了dispatchMessage方法,源码如下
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
void dispatchMessage(Message m) {
String messageJson = m.toJson();
// 为json字符串转义特殊字符
messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
String javascriptCommand = String.format(MyBridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
// 必须要找主线程才会将数据传递出去
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
}
}
可见执行结果通过一些格式处理字符转义之后,以”javascript:WebViewJavascriptBridge._handleMessageFromNative(’{resultData}’);“的形式调用桥接js的_handleMessageFromNative方法(步骤9),
function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
}
_dispatchMessageFromNative(messageJSON);
}
_handleMessageFromNative在将处理结果直接放进messageQueue之后执行了_dispatchMessageFromNative
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
//java call finished, now need to call js callback function
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
//直接发送
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId,
responseData: responseData
});
};
}
var handler = WebViewJavascriptBridge._messageHandler;
if (message.handlerName) {
handler = messageHandlers[message.handlerName];
}
//查找指定handler
try {
handler(message.data, responseCallback);
} catch (exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
}
}
}
});
}
这段会看着非常眼熟,因为他的逻辑跟 步骤7 中CallBackFunction的实现如出一辙,这也可以理解,因为通讯是双向的,自然两边的实现逻辑也就一致,只是实现的语言不一样。
有了上面 步骤7 的分析,这段就好理解多了,因为 步骤7 中内部生成的CallBackFunction为Message指定了responseId 为js传过去的callBackId,那么我们也就能根据这个callBackId对应到 步骤2 中存入responseCallbacks中的responseCallback,也就是调用方js定义的callBack,在获取设备信息的例子汇总该callBack就是
function(responseData) {
document.getElementById("show").innerHTML = "getDeviceInfo involved,responseData from native is " + responseData
}
至此,js调用native的过程我们就分析完了。调用过程在js和native之间来回转换,会有些绕,用图表的方式总结一下可能会更清晰一些: