1 JSBridge介绍
工程地址:https://github.com/lzyzsd/JsBridge
一句话介绍:This project make a bridge between Java and JavaScript.It provides safe and convenient way to call Java code from js and call js code from java.
2 原理
本文主要阐述Android(Java)与H5(JS)交互,以下描述统一用各自语言替代。
2.1 JS调用Java方法
前端页面通过某种方式触发scheme(如用iframe.src),然后Native用某种方法捕获对应的url触发事件,然后拿到当前的触发url,根据定义好的协议,分析当前触发了那种方法,然后根据定义来执行。
JS方法:
//sendMessage add message, 触发native处理 sendMessage
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;
}
Java回调:
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
String theUrl = url;
try {
theUrl = URLDecoder.decode(url, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
if (theUrl.startsWith(BridgeUtil.YY_RETURN_DATA)) {
webView.handlerReturnData(theUrl);
return true;
} else if (theUrl.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, theUrl);
}
}
2.2 Java调用JS方法
若调用的js方法没有返回值,则直接可以调用mWebView.loadUrl("javascript:do()");其中func是js中的方法;若有返回值时可以调用mWebView.evaluateJavascript("javascript:sum()",new ValueCallback<String>() {})方法。
//js方法没有返回值,JSBridge目前采用的方式
mWebView.loadUrl("javascript:func()");
//js方法有返回值
mWebView.evaluateJavascript("sum(1,2)", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//todo
}
});
对应的Js代码如下:
<script type="text/javascript">
function sum(a,b){
return a+b;
}
function do(){
document.getElementById("p").innerHTML="hello world";
}
</script>
3 初始化
Android工程需要引入WebViewJavascriptBridge.js文件,并在BridgeWebViewClient调用onPageFinished方法时进行load:
public static void webViewLoadLocalJs(WebView view, String path){
String jsContent = assetFile2Str(view.getContext(), path);
view.loadUrl("javascript:" + jsContent);
}
WebViewJavascriptBridge.js会执行以下操作来初始化bridge:
var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
init: init,
send: send,
registerHandler: registerHandler,
callHandler: callHandler,
_fetchQueue: _fetchQueue,
_handleMessageFromNative: _handleMessageFromNative
};
var doc = document;
_createQueueReadyIframe(doc);
_createQueueReadyIframe4biz(doc);
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('WebViewJavascriptBridgeReady');
readyEvent.bridge = WebViewJavascriptBridge;
doc.dispatchEvent(readyEvent);
而H5在使用JSBridge时需要先判断WebViewJavascriptBridge是否存在,如果不存在可以监听WebViewJavascriptBridgeReady event:
if (window.WebViewJavascriptBridge) {
//do your work here
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
//do your work here
},
false
);
}
4 交互流程
下面以takePhoto方法为例,通过源码来分析Js调用Java方法,并获取返回数据的流程。
4.1 JS通知Java有消息来了
H5页面点击拍照,JS调用takePhoto方法:
takePhoto(obj) {
return new Promise(function (resolve, reject) {
window.bridge.callHandler('takePhoto', obj, function (res) {
if (typeof res == 'string') {
res = JSON.parse(res)
}
if (res) {
resolve(res)
} else {
reject('Get takePhoto info error.')
}
})
})
},
进入WebViewJavascriptBridge的callHandler及_doSend方法:
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
//sendMessage add message, 触发native处理 sendMessage
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;
}
responseCallback即为JS传入的回调函数(接收原生拍照返回数据),显然不为空。于是生成唯一的callbackId,然后以callbackId为key, 回调函数为value,放在responseCallbacks这个对象里(返回数据时从responseCallbacks找到对应的responseCallback),同时在message这个对象里也存一份。然后再往sendMessageQueue这个数组里push一个message对象。接着会变更messagingIframe元素的的src属性,使得原生回调shouldOverrideUrlLoading。
到这里,JS通知处理完成,Java开始做对应处理。
4.2 Java收到通知,并告诉JS发送消息
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
String theUrl = url;
try {
theUrl = URLDecoder.decode(url, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
if (theUrl.startsWith(BridgeUtil.YY_RETURN_DATA)) {
webView.handlerReturnData(theUrl);
return true;
} else if (theUrl.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, theUrl);
}
}
这里会走第二个if,调用BridgeWebView的flushMessageQueue()方法:
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
//代码太长,此处省略,后面会展示
}
});
}
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
在这个方法里,首先会调用WebViewJavascriptBridge的_fetchQueue()方法,然后解析方法名字,即为_fetchQueue。然后以_fetchQueue为key,新创建的回调方法CallBackFunction为value, 放到responseCallbacks(Java中是Map,注意与JS同名的responseCallbacks区分)里面。
到这里,Java响应了JS的QUEUE_HAS_MESSAGE请求,并通过loadUrl的方式请求JS的_fetchQueue()方法。
4.3 JS响应Java调用,并发送请求数据给Java
// 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
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
if (messageQueueString !== '[]') {
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
}
sendMessageQueue这个数组我们在_doSend()方法中用到过,里面push了一个message对象,json格式化之后字符串就是[{"handlerName":"takePhoto","data":xxxx,"callbackId":"cb_1_1595831484000"}]这样的,然后将sendMessageQueue这个数组置空, 接着再次变更iframe的src属性,触发java的shouldOverrideUrlLoading方法。
到这里,JS响应了Java的_fetchQueue调用,并再次触发Java的shouldOverrideUrlLoading方法(携带数据)。
4.4 Java收到通知,执行原生逻辑,并发送响应数据给JS
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
String theUrl = url;
try {
theUrl = URLDecoder.decode(url, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
if (theUrl.startsWith(BridgeUtil.YY_RETURN_DATA)) {
webView.handlerReturnData(theUrl);
return true;
} else if (theUrl.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, theUrl);
}
}
首先将url解码,这里会走第一个if,调用handlerReturnData(theUrl)方法:
void handlerReturnData(String url) {
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
CallBackFunction f = responseCallbacks.get(functionName);
String data = BridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
f.onCallBack(data);
responseCallbacks.remove(functionName);
return;
}
}
functionName就是_fetchQueue,根据方法名从responseCallbacks这个Map中取出回调方法, 然后解析出data并调用回调方法,最后将这个回调方法从Map中移除。
接着我们看onCallBack中的逻辑(补上flushMessageQueue函数未展示部分):
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
// deserializeMessage 反序列化消息
List<Message> 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
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);
}
}
}
}
});
}
}
首先将数据解析成一个Message的list,数据类似[{"handlerName":"takePhoto","data":xxxx,"callbackId":"cb_1_1595831484000"}]。
接着遍历这个list中的每一个Message,Message类定义如下:
public class Message {
private String callbackId; //callbackId
private String responseId; //responseId
private String responseData; //responseData
private String data; //data of message
private String handlerName; //name of handler
...
}
这里只有callbackId,没有responseId,因此会走else分支的里面的if分支,即新建一个CallBackFunction,然后根据Message中的handlerName,从messageHandlers中获取一个BridgeHandler对象,而这个对象需要在Java中注册:
webView.registerHandler("takePhoto", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
...
function.onCallBack(str);
}
});
public void registerHandler(String handlerName, BridgeHandler handler) {
if (handler != null) {
messageHandlers.put(handlerName, handler);
}
}
因此通过messageHandlers.get(m.getHandlerName())拿到的就是registerHandler时的BridgeHandler,进一步执行handler方法,而这里的function就是前面新建的CallBackFunction。继续执行onCallBack:
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
}
};
new一个responseMsg,并设置了responseId和responseData,然后调用了queueMessage()方法,进一步执行dispatchMessage:
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
void dispatchMessage(Message m) {
String messageJson = m.toJson();
//escape special characters for json string 为json字符串转义特殊字符
messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'");
String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
}
}
这里再次通过loadUrl的方式,调用WebViewJavascriptBridge.js的_handleMessageFromNative方法。
4.5 JS响应Java调用,执行回调
function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue && receiveMessageQueue.length > 0) {
receiveMessageQueue.push(messageJSON);
} else {
_dispatchMessageFromNative(messageJSON);
}
}
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
// {"responseData":"http:\/\/ww3.sinaimg.cn\/mw690\/96a29af5jw8fdfu43tnvlj20ro0rotab.jpg","responseId":"cb_1_1532864396479"}
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);
}
}
}
});
}
这里的messageJSON类似{"responseData":xxxx,"responseId":"cb_1_1595831484000"}, responseData和responseId是创建message时设置的。根据responseId从responseCallbacks(这是JS中的数组,别搞混了)中取出responseCallback,这个responseCallback是在_doSend()方法中存进去的,也就是一开始js中callHandler时传进去的一个方法。将message.responseData传递给这个方法,执行完之后从responseCallbacks这个对象里删除responseCallback方法。至此,JS最终执行了自己的回调function:
takePhoto(obj) {
return new Promise(function (resolve, reject) {
window.bridge.callHandler('takePhoto', obj, function (res) {
if (typeof res == 'string') {
res = JSON.parse(res)
}
if (res) {
resolve(res)
} else {
reject('Get takePhoto info error.')
}
})
})
},
4.6 流程总结
整个过程主要分为5步,总结起来就是JS做完Java做,Java做完JS做...,而两者通过loadurl和shouldOverrideUrlLoading的方式进行交互,附一张网上的图:
JSBridge
5 存在的问题
BridgeWebView调用dispatchMessage方法时(交互流程4.4),通过loadUrl的方式将数据传递给Js,而loadUrl会自动进行一次urldecode,导致传递给Js的数据与实际不一致,进而出现一些诡异的bug。
void dispatchMessage(Message m) {
String messageJson = m.toJson();
//escape special characters for json string 为json字符串转义特殊字符
messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'");
String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
// 必须要找主线程才会将数据传递出去 --- 划重点
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
}
}
解决方案:android4.4以及以上采用evaluateJavascript() ,android4.4以下针对不同问题需要特殊处理。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
this.evaluateJavascript(javascriptCommand,null);
} else {
this.loadUrl(javascriptCommand);
}
6 参考文章
本文部分内容参考以下文章:
Android和H5交互-基础篇 https://www.jianshu.com/p/a25907862523
JsBridge源码分析 https://www.jianshu.com/p/c09667ec4c84
网友评论