作者:二两五花肉
链接:
https://kaelinvoker.github.io/Blog/
app的快速迭代,离不开h5的支持。而如何解决Js和Native的通信问题,就需要JsBridge来解决了。本文主要是对第三方库JsBridge的源码解析。
项目地址
https://github.com/lzyzsd/JsBridge
Js通知Native
Js通知Native目前有三种方案。
API注入。通过webview.addJavascriptInterface()的方法实现。
拦截Js的alert/confirm/prompt/console等事件。由于prompt事件在js中很少使用,所以一般是拦截该事件。这些事件在WebChromeClient都有对应的方法回调(onConsoleMessage,onJsPrompt,onJsAlert,onJsConfirm)
url跳转拦截,对应WebViewClient的shouldOverrideUrlLoading()方法。
第一种方法,由于webview在4.2以下的安全问题,所以有版本兼容问题。后两种方法原理本质上是一样的,都是通过对webview信息冒泡传递的拦截,通过定制协议-拦截协议-解析方法名参数-执行方法-回调。
Native通知Js
webview可以通过loadUrl()的方法直接调用。在4.4以上还可以通过evaluateJavascript()方法获取js方法的返回值。
4.4以前,如果想获取方法的返回值,就需要通过上面的对webview信息冒泡传递拦截的方式来实现。
JsBridge源码解析
我们项目是采用url跳转拦截的方式实现Native与Js的通信。
JsBridge的接入
JsBridge的接入分为两部分,H5端的接入,客户端的接入。
客户端的接入。
BridgeWebView这个类
//js注入的文件名
String toLoadJs = "WebViewJavascriptBridge.js";
//白名单的实现接口 由外部自己实现
private IBridgeAccept mAccept;
public void onPageFinished(WebView view, String url) {
//String toLoadJs = "WebViewJavascriptBridge.js";
if (mAccept != null && mAccept.accept(url) && toLoadJs != null ) {
BridgeUtil.webViewLoadLocalJs(view, toLoadJs);
}
}
BridgeUtil的webViewLoadLocalJs方法
public static void webViewLoadLocalJs(WebView view, String path) {
//如果为空,则去assets目录下读取js文件
if (null == jsContent) {
jsContent = assetFile2Str(view.getContext(), path);
}
view.loadUrl("javascript:" + jsContent);
}
从上面的代码可以看出,JsBridge实现的JS部分代码也是放在了客户端的assets目录下。在页面加载完成后,由客户端主动注入。并且这里存在域名白名单机制,只有在白名单内才会注入js代码。
H5端的接入
由于客户端注入js是异步的,H5调用方法时,必须要确保js代码注入成功。因此必须要监听注入事件,成功后通知H5。
注入的WebViewJavascriptBridge.js部分代码:
//自定义jsBridge初始化事件WebViewJavascriptBridgeReady,并在最后主动触发事件
//通知h5jsBridge初始化完毕
var doc = document;,
_createQueueReadyIframe(doc);
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('WebViewJavascriptBridgeReady');
readyEvent.bridge = WebViewJavascriptBridge;
doc.dispatchEvent(readyEvent);
在h5调用Bridge方法时,需要监听Bridge初始化事件
//已经初始化了
if (window.WebViewJavascriptBridge) {
//do your work here
} else {
//监听初始化事件
document.addEventListener(
'WebViewJavascriptBridgeReady', function() {
//do your work here
}, false);
}
Native与Js通信的流程
Native调用Js
先看一张我画的流程图
举个例子:
webview.callHandler("functionInJs", "哈哈我是java传来的", new CallBackFunction() {
@Override
public void onCallBack(String data) {
Log.e("MainActivity", "reponse data from js " + data);
}
@Override
public void onFailed(String data) {
super.onFailed(data);
Log.e("MainActivity", "onFailed data from js " + data);
}
});
看下这个方法做了什么:
//java调用Js
//handlerName是Js提前注册的方法名 data是方法的参数
//callBack 是java的回调对象
public void callHandler(String handlerName, String data,
CallBackFunction callBack) {
doSend(handlerName, data, callBack);
}
这里是流程的入口,native调用js提前预注册的方法,这里注意方法名一定要和JS预定义的方法名完全相同,才会调用成功。
private void doSend(String handlerName, String data,
CallBackFunction responseCallback) {
//将要调用的js方法名和参数封装成Message
Message m = new Message();
if (!TextUtils.isEmpty(data)) {
m.setData(data);
}
//如果java需要回调,生成唯一的回调id,放到message中
//并且将对应的java回调保存到 responseCallbacks中,以callbackId为键
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);
}
在doSend函数中,主要做的就是将方法名、参数、回调id封装message。
private void queueMessage(Message m) {
//startupMessage在页面第一次加载完成就会置空,所以这里一定会走到dispatchMessage中
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
private void dispatchMessage(Message m) {
//message转成json,并进行转义
String messageJson = m.toJson();
messageJson = messageJson.replaceAll("\\\\/", "/");
messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
//将转义好的消息json串传递到js的_handleMessageFromNative方法中
//并在主线程中调用
String javascriptCommand = String.format(
BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
messageJson = null;
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
WYLogUtils.i(TAG, "dispatchMessage --> " + javascriptCommand);
this.loadUrl(javascriptCommand);
}
}
这里的BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA是js的方法名,native可以通过loadUrl()的方式调用
final static String JS_HANDLE_MESSAGE_FROM_JAVA
= "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";
到这里就把要调用的方法名,参数,是否需要回调的消息就传给了js
再看下js的_handleMessageFromNative做了什么:
function _handleMessageFromNative(messageJSON) {
//receiveMessageQueue在页面加载完成后就赋值为null了
//所以最后都会到_dispatchMessageFromNative中
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
} else {
_dispatchMessageFromNative(messageJSON);
}
}
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
//将传递的消息json串转为message对象
var message = JSON.parse(messageJSON);
var responseCallback;
//java调用js 并没有responseId 所以走else
//这里是js调用java并且js有回调,这里表明java将js回调需要的数据传过来了
//此时再进行js 回调处理
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
//直接发送
这里如果有回调id, 说明前面java需要js回传数据
//构造回调对象
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId,
responseData: responseData
});
};
}
//从js预定义的方法集合messageHandlers中,匹配传递过来消息中方法名
var handler = WebViewJavascriptBridge._messageHandler;
if (message.handlerName) {
handler = messageHandlers[message.handlerName];
}
//匹配成功后,调用该方法,传递数据,并且将回调对象作为参数也传递了
try {
handler(message.data, responseCallback);
} catch (exception) {
if(responseCallback){
responseCallback({error:404,errorMessage:"JS API not find"})
}
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
}
}
}
});
}
这里的messageHandlers是从哪来的?看下面的代码
//定义方法集对象
var messageHandlers = {};
//js注册方法时,其实就是以注册的方法名为key, 具体的方法对象为值存储到messageHandler中
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
到这里,native已经成功调用js的方法了。但是如果java还需要js回传数据的话,那么在js注册的方法中,需要主动使用上面传递进参数的回调对象调用其方法才可以完成。
如这样。
//js 注册了一个名为functionInJs的方法 这里的responseCallback就是传递的回调对象
bridge.registerHandler("functionInJs", function(data, responseCallback) {
document.getElementById("show").innerHTML = ("data from Java: = " + data);
/因为java不一定需要js回传数据,只有需要的时候,才会传入该对象,所以这里判空
if (responseCallback) {
var responseData = {data:'Javascript Says Right back aka!'};
//使用该回调对象主动调用其方法
responseCallback(responseData);
}
});
主动调用该方法后,我看回看上面的代码。
if (message.callbackId) {
var callbackResponseId = message.callbackId;
//其实就是调用该方法
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId,
responseData: responseData
});
};
}
上面分析过,这里是如果java需要js回传数据,也就是message中有callbackId,会创建回调对象。而这时h5端主动调用该回调的对象的方法,其实就是走到了这里。会把回传的数据和该callbackId封装成message对象,而callbackId这时就成了responseId。这里其实也是为了在java端能够根据id取出最初存入的回调。
function _doSend(message, responseCallback) {
//这时的responseCallback为null
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
//将上步的message放入sendMessageQueue发送消息队列中
sendMessageQueue.push(message);
//改变iframe的src从而通知native从h5取消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
这里的messagingIframe是什么呢?看注入的js代码。
//声明iframe元素
var messagingIframe;
//创建不可见的的ifrmae
function _createQueueReadyIframe(doc) {
messagingIframe = doc.createElement('iframe');
messagingIframe.style.display = 'none';
doc.documentElement.appendChild(messagingIframe);
}
通过改变iframe的src会触发WebViewClient的shouldOverrideUrlLoading()。这里可以看到上面将iframe的src改为yy://__QUEUE_MESSAGE__/,通知Native去取消息。
//通知Native有消息的协议
yy://__QUEUE_MESSAGE__/
下面看看Native做了什么
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
WYLogUtils.i(TAG, "shouldOverrideUrlLoading --> " + url);
//yy://return/{function}/returncontent || yy://"
//这里开始拦截协定的协议
if (url.startsWith(BridgeUtil.YY_RETURN_DATA) || url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
//解码
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (IllegalArgumentException e) { //解决未编码同时内容中有%时奔溃的问题
WYLogUtils.e(TAG, e.getMessage(), e);
if (url.contains("%%")) {
try {
url = URLDecoder.decode(removeDoublePercent(url), "UTF-8");
} catch (Exception e1) {
WYLogUtils.e(TAG, e1.getMessage(), e1);
}
}
} catch (Exception e) {
WYLogUtils.e(TAG, e.getMessage(), e);
}
//yy://return/{function}/returncontent || yy://
//这里由于上面是为yy://__QUEUE_MESSAGE__/ 所以会走到flushMessageQueue()中
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {// 如果是返回数据
handlerReturnData(url);
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
flushMessageQueue();
}
return true;
}
...
}
这里拦截到预定的协议后,调用了flushMessageQueue()
private void flushMessageQueue() {
//这里在主线程
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
//这里往loadUrl方法中传了一个字符串和一个新的回调对象
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA,new CallBackFunction() {
@Override
public void onCallBack(String data) {
...
}
}
再看下BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA是什么
//这是个js方法
final static String JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();";
再看下这里的loadUrl(String jsUrl, CallBackFunction returnCallback)方法做了什么
private void loadUrl(String jsUrl, CallBackFunction returnCallback) {
//主线程执行了js的_fetchQueue方法
this.loadUrl(jsUrl);
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl),
returnCallback);
}
看下BridgeUtil.parseFunctionName(jsUrl)是什么
//从字符串中解析出js的方法名
public static String parseFunctionName(String jsUrl) {
return jsUrl.replace("javascript:WebViewJavascriptBridge.", "").replaceAll("\\(.*\\);", "");
}
所以在loadUrl方法中做了两件事。第一执行_fetchQueue方法,第二以_fetchQueue为key,将上面创建的回调对象为值,存到了responseCallbacks中。
下面看看_fetchQueue方法
function _fetchQueue() {
//将上面要回传给native的消息sendMessageQueue转为json
//所有的消息
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//这里肯定是Android Ios多余 用的不是一个框架
if(isAndroid()){
//通知Native过来拿消息。
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + messageQueueString;
}else if (isIphone()) {
return messageQueueString;
//android can't read directly the return data, so we can reload iframe src to communicate with java
}
}
这里将要回传给native的消息转为json,放到iframe的src中,从而通知native取消息。
//返回数据的协议
yy:///return/_fetchQueue/+json
触发WebViewClient的shouldOverrideUrlLoading(),拦截该url,并调用handlerReturnData(url)
final static String YY_OVERRIDE_SCHEMA = "yy://";
final static String YY_RETURN_DATA = YY_OVERRIDE_SCHEMA + "return/";/
//shouldOverrideUrlLoading方法中的代码片段
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {// 如果是返回数据
handlerReturnData(url);
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
flushMessageQueue();
}
handlerReturnData方法中处理回传的数据,并执行前面存入回调对象的方法。
private void handlerReturnData(String url) {
//functionName: _fetchQueue
//以_fetchQueue为key从responseCallbacks中取出上面存入的回调对象
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
CallBackFunction f = responseCallbacks.get(functionName);
//从回传数据中解析出回传的数据
String data = BridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
//执行回调对象的方法,并传入数据
f.onCallBack(data);
//移除回调
responseCallbacks.remove(functionName);
}
}
回调对象的方法看看做了什么
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA,
new CallBackFunction() {
@Override
public void onCallBack(String data) {
List<Message> list = null;
try {
//将回传的data转成Message集合
list = Message.toArrayList(data);
} catch (Exception e) {
WYLogUtils.e(TAG, e.getMessage(), e);
}
if (list == null || list.size() == 0) {
return;
}
//由于在js _fetchQueue方法中是将整个消息队列发送过来 遍历集合,这里面有我们分析的那条消息
for (int i = 0; i < list.size(); i++) {
Message m = list.get(i);
//分析的那条消息带有responseId
String responseId = m.getResponseId();
//说明java调用了js 且需要js回传数据
if (!TextUtils.isEmpty(responseId)) {
//而这个responseId其实就是一开始java需要回调时生成的callbackId
//根据这个callbackId从responseCallbacks取出最开始存入的回调对象 并且执行该回调方法
CallBackFunction function = responseCallbacks
.get(responseId);
String responseData = m.getResponseData();
function.onInnerCallBack(responseData);
//执行后移除该回调对象
responseCallbacks.remove(responseId);
}else{
.....
}
}
}
}
);
再看下回调对象这个类
public abstract class CallBackFunction {
//执行回调的方法
public void onInnerCallBack(String data){
//将js回传的消息转成json,并解析error属性,默认是200表明调用js方法成功
//如果解析出404表明调用js方法失败,
if(data != null && data.length()>0){
try {
JSONObject obj = new JSONObject(data);
int resultCode = obj.optInt("error",200);
if(resultCode == 404){
onFailed(data);
}else{
onCallBack(data);
}
} catch (JSONException e) {
e.printStackTrace();
}
}else{
onCallBack(data);
}
}
//抽象方法,native注册方法时的回调对象的回调方法
public abstract void onCallBack(String data);
public void onFailed(String data){
}
}
到这里Native调用js分析完毕。从整个流程来看,Native消息通知JS很方便,直接调用就好了。而Js由于不能直接通知给Native,所以相对来说比较绕,得先通过协议拦截的方式通知Native JS有消息要传递,然后Native再主动调用Js的方法,将Js要传递的消息再通过协议拦截的方式传递给Native。
Js调用Native
还是看张我画的流程图。
js调用Native的流程其实和Native调用Js基本是相似的,所以这里就简单介绍了。
依然是举个例子
window.WebViewJavascriptBridge.callHandler(
'callNative'
, {'param': '哈哈哈哈'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
js调用callHandler方法,将要调用的方法名和参数以及回调对象传入doSend()方法中。
function _doSend(message, responseCallback) {
//js如果需要回调 生成callbackId与方法名,参数封装成message
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
//将Callback保存到responseCallbacks 以callbackId为key
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
//将message放到消息队列中
sendMessageQueue.push(message);
//通知Native过来取消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
native拦截该协议,会调用flushMessageQueue,这里和上面Java调用js一样。webview通过loadUrl的方式,调用js的_fetchQueue方法。同时生成回调对象,保存到responseCallbacks中。
f_fetchQueue方法上面也分析过了,会将js的消息队列转为json放到iframe的src中,然后再次通知Native过来取消息。
Native拦截收到消息后,调用handlerReturnData()方法。从responseCallbacks取出上面注册的回调对象,调用其方法,并移除该回调对象。
下面看回调方法里面做了什么。
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA,
new CallBackFunction() {
@Override
public void onCallBack(String data) {
//这里和前文一样,取出js传过来的消息
//将消息转为message集合 遍历集合
....
for (int i = 0; i < list.size(); i++) {
Message m = list.get(i);
//这里和前文一样 由于这次js传过来的消息里并没有responseId 所以直接看else
String responseId = m.getResponseId();
if (!TextUtils.isEmpty(responseId)) {
...
}else{
//js 调用了 java
//如果有callbackId,说明js要回传数据
//创建回调函数
CallBackFunction responseFunction = null;
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) {
//no-op
}
}
}
//从消息中取出方法名去匹配native提前的注册的方法池
//如果存在就取出方法对象并回调该方法。
BridgeHandler handler;
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
if (handler == null) {
handler = defaultHandler;
}
} else {
handler = defaultHandler;
}
handler.handler(specialCharacterReplace(m.getData()), responseFunction);
}
}
});
下面再分析java数据如何回传的。上面已经分析了,如果js需要Java回传数据,是会创建一个回调对象,并传入的要调用的Handler的方法中。java端就可以使用该对象调用其方法来进行回传数据。
看看该方法做了什么。
//将要回传的数据封装成message,原先从js传过来的callbackId变成responseId
Message responseMsg = new Message();
//设置responseId
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
继而调用queueMessage()方法,最终会调到dispatchMessage。这里就和前文分析的一样,会将传过来的message转成json,再进行转义,随后在主线程调用JS的_handleMessageFromNative方法,并将json传递过去。
_handleMessageFromNative()方法又会调用_dispatchMessageFromNative()方法。
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
//java call finished, now need to call js callback function
//js 调用 java 并且js有回调,这里表明 java将js回调需要的数据传过来了 此时再进行js 回调处理
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
...
}
});
}
至此,Js调用Native完毕。
分析源码后,可以看到Bridge目前存在以下问题
Js调用java偶现调用失败。因为通过iframe的机制并不能保证shouldOverrideUrlLoading每次都会调用。
部分手机java调用js偶现失败。
推测有两个原因:
webview在调用js方法时,只在主线程调用。如果不在主线程的时候,就不会调用了;
Js注入的时机问题。在OnPageFinished中注入虽然最后都会全局注入成功,但是完成时间有可能太晚。