在 Android 中 WebView 是与 JS 交互的一个桥梁, JsBrigde 的本质也是通过 WebView 的方法调用的 JS ,JS 同样可以调 WebView。
对于 Android 调用 JS 码的方法有 2 种:
1.通过 WebView 的 loadUrl()
2.通过 WebView 的 evaluateJavascript()
对于 JS 调用 Android 代码的方法有 3 种:
1.通过 WebView 的 addJavascriptInterface() 进行对象映射
2.通过 WebViewClient 的 shouldOverrideUrlLoading () 方法回调拦截 url
3.通过 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截JS对话框 alert()、confirm()、prompt() 消息
在 JsBrigde 库中 Native 的部分就是通过 webView.loadUrl() 来调用的 JS 代码,而 JS 的部分则是通过刷新 ifream.src 属性来通知 WebViewClient 的 shouldOverrideUrlLoading ()方法,把数据传递到 Native 的。双方使用 json 格式来传递数据。
JsBrigde源码分析
无论是 JS 调 Native,还是 Native 调 JS,两边要约定好 handlerName,调用方传递的 handlerName,接收方同样需要一致的 handlerName,来保证双方能够正常通信。
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': '中文测试'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
webView.registerHandler("submitFromWeb", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
Log.e(TAG, "handler = submitFromWeb, data from web = " + data);
function.onCallBack("submitFromWeb exe, response data 中文 from Java");
}
});
Js -> Native -> Js
下面这张图就是 Js -> Native -> Js 的一个过程,可以结合这张图,来看下面的分析
先以 JS 调 Native 为例,在 H5 中想调 Native 方法 ,执行 WebViewJavascriptBridge.callHandler 方法,此方法有三个参数,分别是: 与 Native 约定好的 handlerName,要发送的数据data,及用来接收 Native 返回数据的 responseCallBack,在 callHandler 方法中就干了一件事调用 _doSend 方法
//JS call native method
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': '中文测试'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
在 _doSend 方法中,首先判断了有没有 responseCallback,如果有,创建一个callbackId 赋值于 Message 对象,创建 responseCallBack 数组存储 responseCallback,创建 MessageQueue 添加 Message,然后调用 Iframe.src 属性,刷新这个属性时,会走到 Native 中的 BridgeWebViewClient.shouldOverrideUrlLoading 方法,此方法可以拦截 URL
//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;
}
在 BridgeWebViewClient 的 shouldOverrideUrlLoading 方法中可以拿到 JS 中 ifream.src 刷新后的 URL,根据URL 的前缀调用了 webView.flushMessageQueue();
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
在 flushMessageQueue 方法中先不看 CallBackFunction 的实现,在这里调用了 webView 的 loadUrl() 方法来调 JS 的 _fetchQueue();
//Js 方法
String JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();"
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
}
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
// 添加至 Map<String, CallBackFunction>
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
在这里再次调用了Iframe.src,把 _doSend 方法存储的 Message 数组转换成 json 字符串,然后拼接成了 URL 回传到 Native,这时再次走到 BridgeWebViewClient.shouldOverrideUrlLoading 方法
// 提供给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
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
这次根据URL前缀会调用 handlerReturnData(url)方法,并把URL数据传过去。
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
在这里解析了URL,取出 functionName,CallBackFunction 及 data,并把 data 塞给 CallBackFunction。
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;
}
}
现在来看 CallBackFunction 的实现,这代码有点长,不过这次调用只会走一部分代码,首先把 data 转成了 list,并且遍历这个 list 取出 Message 及 Message 中的 responseId,这个时候 responseId 应该是不存在的,因为这时候 js 传递过来的数据就没有 responseId,可以回到 _doSend 方法看一下,Message 对象只存储了callbackId,所以会走到 else 里,在这里取 callbackId 判断是否为空,如果不为空,说明 JS 在调用 Native 方法后还需要 Native 这边的 “反馈”,在 callHandler 是就说了第三个参数 responseCallBack 是用来接受 Native 返回来数据的回调, 那么这时候实现了 Native 外部 registerHandler 该方法的 CallBackFunction 接口,把 data 数据封装成 Message 对象,给 Message 对象设置了一个 responseId, 接着调用了 queueMessage(responseMsg); 如果为空就说明 JS 不需要 Native 的数据回调,而CallBackFunction也是空实现,最终根据 handlerName 取出来 Native 对应注册的方法,把数据传给上层,这样上层的registerHandler方法就收到 JS 传过来的 data 数据了。
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 如果有回调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 还没收到 Native 的数据回调,接着看 queueMessage(responseMsg) , 直接调了 dispatchMessage,在 dispatchMessage 中,把 Message 转换 Json 字符串,拼接了 JS指令 ,通过 webview.loadUrl() 调用了 JS 方法 _handleMessageFromNative
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";
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);
}
}
_handleMessageFromNative 调用了 _dispatchMessageFromNative 方法,同样这段代码也很长,仔细看的话,这段 JS 代码和 Native 的 CallBackFunction 里的代码逻辑上很像,这里依然只会走一部分代码,在这里也会先取Message对象里的 responseId, 很明显Native 传过来的 Message 对象中有 responseId,所以在这里就回调给了上层,callHandler 中的 responseCallBack 回调就有值了。下面的 else 也不会执行。
//提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
}
_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
});
};
}
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);
}
}
}
});
}
到此 JS 调用 Native 方法并且得到 Native 返回的数据回调,整个流程就走了一遍,反过来其实也是一样的,这里就不再跟进了,可以参考下面这张图自己屡一下。刚才说的两段比较长的代码中,其实两边调用都走了这两段代码,每次单项调用就会走方法中对应的某一部分。