JSBrige系列直通车,由浅入深理解JS-Native的通信过程:
JSbridge系列解析(一):JS-Native调用方法
JSbridge系列解析(二):lzyzsd/JsBridge使用方法
JSbridge系列解析(三):lzyzsd/JsBridge源码解析
JSbridge系列解析(四):Web端发消息给Native代码流程具体分析
先说结论吧:必须在主线程中调用BridgeWebView的CallHandler方法,否则可能无效。
开发中遇到一个场景,需要在上传图片过程中,调用CallHandler后,向JS代码传递上传进度。但实际测试发现,JS未触发。下面我们看下JsBridge的实现源码来分析该问题的原因。
Java代码调用JS
调用流程引用简书中某作者图,具体可见http://www.jianshu.com/p/fce3e2f9cabc
以Demo中MainActivity中WebView.CallHandler为例,调用流程比较简单直接,不做过多讲解。直接看源码,如下:
/**
* call javascript registered handler
*
* @param handlerName 方法名
* @param data 入参,一般为和服务器约定的gson对象
* @param callBack 回调函数
*/
public void callHandler(String handlerName, String data, CallBackFunction callBack) {
doSend(handlerName, data, callBack);
}
//1)组装Message对象;2)强回调函数保存在responseCallbacks中
private void doSend(String handlerName, String data, CallBackFunction responseCallback) {
Message m = new Message();
if (!TextUtils.isEmpty(data)) {
m.setData(data);
}
if (responseCallback != null) {
String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));
responseCallbacks.put(callbackStr, responseCallback);
m.setCallbackId(callbackStr);
}
if (!TextUtils.isEmpty(handlerName)) {
m.setHandlerName(handlerName);
}
queueMessage(m);
}
//将消息加入队列。若队列为空,则直接分发运行
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
//1)将Message对象转为JS语句,2)通过loadUrl执行JS代码,触发lib库中的_handleMessageFromNative方法
void dispatchMessage(Message m) {
String messageJson = m.toJson();
//escape special characters for json string
messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
}
}
接下来我们着重看下queueMessage方法,心里会有这样的疑问:startupMessage对象在什么情况下会为空呢。看BridgeWebview的成员变量声明语句,startupMessage在声明时已被初始化。
//BridgeWebView.java
private List<Message> startupMessage = new ArrayList<Message>();
在BridgeWebView中,startupMessage并未被赋值为空;这就只能全局搜索startupMessage的引用了。最终在BridgeWebViewClient的onPageFinished方法中找到。
//BridgeWebViewClient.java
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
//加载asset目录下的WebViewJavascriptBridge.js文件
if (BridgeWebView.toLoadJs != null) {
BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
}
//将startupMessage队列中的所有消息执行,并将队列置为空
if (webView.getStartupMessage() != null) {
for (Message m : webView.getStartupMessage()) {
webView.dispatchMessage(m);
}
webView.setStartupMessage(null);
}
}
根据我的理解,startupMessage队列主要是用来在JsBridge的js库注入之前,保存Java调用JS的消息,避免消息的丢失或失效。待页面加载完成后,后续CallHandler的调用,可直接使用loadUrl方法而不需入队。究其根本,是因为Js代码库必须在onPageFinished(页面加载完成)中才能注入导致的。
再回到文章头部的问题,开发中页面加载完成,此时startupMessage队列为空。用户选择图片上传时,由于上传是异步线程,进度回调也运行在非ui线程。当调用BridgeWebView的dispatchMessage方法时,因当前线程为非主线程,导致并未触发loadUrl。解决方法时必须在主线程中调用CallHandler方法
WebViewJavascriptBridge.js实现
接下来看JsBridge库中WebViewJavascriptBridge.js代码,CallHandler方法最终会执行js中_handleMessageFromNative方法
//提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以最终调用了_dispatchMessageFromNative方法
function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue && receiveMessageQueue.length > 0) {
receiveMessageQueue.push(messageJSON);
} else {
_dispatchMessageFromNative(messageJSON);
}
}
//提供给native使用,
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
});
};
}
//获取默认handler。若message设置了handlerName,则在messageHandlers中依据名字获取
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);
}
}
}
});
}
//存储注册的Handler(assigned handlerName)
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
根据CallHandler调用过程中Message的创建代码,其responseId为null,故最终调用handler = messageHandlers[message.handlerName]。该队列中存储Js注册给Java调用的Handler方法,即源码示例的demo.html文件中的functionInJs。
//demo.html
connectWebViewJavascriptBridge(function(bridge) {
//初始化,设置WebViewJavascriptBridge._messageHandler
bridge.init(function(message, responseCallback) {
console.log('JS got a message', message);
var data = {
'Javascript Responds': '测试中文!'
};
console.log('JS responding with', data);
responseCallback(data);
});
//注册方法供java调用
bridge.registerHandler("functionInJs", function(data, responseCallback) {
document.getElementById("show").innerHTML = ("data from Java: = " + data);
var responseData = "Javascript Says Right back aka!";
responseCallback(responseData);
});
})
JS代码调用Java
实现原理:利用js的iFrame(不显示)的src动态变化,触发java层WebViewClient的shouldOverrideUrlLoading方法,然后让本地去调用javasript。
JS代码执行完成后,最终调用_doSend方法处理回调。
//sendMessage add message, 触发native处理 sendMessage.【JS代码】
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;
}
iFrame变更后,java部分触发shouldOverrideUrlLoading方法,根据scheme不同,进入webview的flushMessageQueue方法。该方法最终调用JS的_fetchQueue方法。
// 提供给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
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
上述方法运行后,iFrame再次变更,java部分触发shouldOverrideUrlLoading方法,根据scheme不同,进入webview的handlerReturnData方法,实现java回调函数的调用。
疑问:目前还未想明白_doSend为什么不直接调用_fetchQueue,而必须通过Java代码转一圈。后续明白了再补充吧。
目前想到的使用_fetchQueue的一个优点,可以批量处理Message。因_doSend中将待处理的message放入sendMessageQueue,而_fetchQueue中将队列中消息全部取出转为json数据传递给WebViewClient。