JsBridge 源码解析

本文简单介绍了 JsBridge 的使用,并基于源码详细分析了 JS 和 Native 的相互调用流程。最后,简单谈了谈对 JsBridge 的一些理解,包括其通信机制、实现逻辑等。

项目简介

项目地址:https://github.com/lzyzsd/JsBridge

JsBridge 是一座用 JavaScript 搭建起来的桥,桥的两端分别是 Native 和 Web。这座桥实现了 Java 和 JavaScript 的数据交换,即 Native 端可以调 Web 的 JS 代码,JS 端也可以调用 Native 的原生代码。

简单使用

使用说明

这个库会在 onPageFinished 加载一个 JS 文件 WebViewJavascriptBridge.js,这会向 window 对象注入一个 WebViewJavascriptBridge 对象,因此在 JS 中使用 WebViewJavascriptBridge 对象时需检查它是否已经注入,可以通过监听 WebViewJavascriptBridgeReady 事件来实现。

function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge);
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
callback(WebViewJavascriptBridge)
},
false
);
}
}

JS 调 Native

以下是一个例子,实现按下物理返回键时响应 WebView 的 JS 方法。

首先,在 Native 端通过 registerHandler 方法注册一个设置物理返回键监听的接口供 JS 调用,当按下物理返回键时执行 JS 回调:

webview.registerHandler("setPhysicalBackListener", new BridgeHandler() {
@Override
public void handler(final String data, final CallBackFunction function) {
// set physical back key listener
// execute function.onCallBack(responseData) when user click physical back key
......
}
});

然后,在 JS 端通过 callHandler 方法调用 Native:

connectWebViewJavascriptBridge(function(bridge) {
bridge.callHandler(
'setPhysicalBackListener',
{},
function(responseData) {
alert("back key pressed " + responseData);
}
);
});

JSBridge 也提供了无方法名的调用方式。

首先需要在 Native 端设置一个默认的 Handler:

webView.setDefaultHandler(new DefaultHandler());

然后,在 JS 端通过 send 方法调用 Native:

window.WebViewJavascriptBridge.send(
data,
function(responseData) {
}
);

Native 调 JS

以下是一个例子,实现在 Native 端调用在 JS 中定义的 JS 函数。

首先,在 JS 端通过 registerHandler 方法注册一个 Handler:

connectWebViewJavascriptBridge(function(bridge) {
bridge.registerHandler(
"functionInJs",
function(data, responseCallback) {
document.getElementById("show").innerHTML = ("data from Java: = " + data);
if (responseCallback) {
var responseData = "Javascript Says Right back aka!";
responseCallback(responseData);
}
}
);
});

然后,在 Native 端通过 callHandler 方法调用:

webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {
@Override
public void onCallBack(String data) {
Log.i(TAG, "reponse data from js " + data);
}
});

同样的,JSBridge 也提供了无方法名的调用方式。

首先需要在 JS 端设置一个默认的 Handler:

connectWebViewJavascriptBridge(function(bridge) {
bridge.init(function(message, responseCallback) {
console.log('JS got a message', message);
var data = {
'Javascript Responds': 'hello'
};
if (responseCallback) {
console.log('JS responding with', data);
responseCallback(data);
}
});
});

然后,在 Native 端通过 send 方法调用:

webView.send("hello");

源码解析

核心类分析

核心的 Java 类主要有两个:BridgeWebViewBridgeWebViewClient

另外,还有一个 WebViewJavascriptBridge.js,主要的 JS 代码都在这个 JS 文件里。

先来看 BridgeWebView 的构造函数:

public BridgeWebView(Context context) {
super(context);
init();
}

private void init() {
this.setVerticalScrollBarEnabled(false);
this.setHorizontalScrollBarEnabled(false);
this.getSettings().setJavaScriptEnabled(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
this.setWebViewClient(generateBridgeWebViewClient());
}

protected BridgeWebViewClient generateBridgeWebViewClient() {
return new BridgeWebViewClient(this);
}

初始化时设置了一个自定义的 WebViewClient。

接下来看看 BridgeWebViewClient 这个类:

onPageFinished 加载 JS 文件 WebViewJavascriptBridge.js,如果有未发送的消息,挨个取出进行分发。

@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);

if (BridgeWebView.toLoadJs != null) {
BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
}

if (webView.getStartupMessage() != null) {
for (Message m : webView.getStartupMessage()) {
webView.dispatchMessage(m);
}
webView.setStartupMessage(null);
}
}

shouldOverrideUrlLoading 方法拦截特殊格式的 url,执行 Native 代码。

@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 this.onCustomShouldOverrideUrlLoading(url) ? true : super.shouldOverrideUrlLoading(view, url);
}
}

下面着重分析一下 JS 和 Native 相互调用的流程。

JS 调 Native 流程分析

首先,在 Native 端通过 registerHandler 方法注册一个特定 Handler,或者通过 setdefaultHandler 方法注册一个默认 Handler。这里使用了一个 Map 对象保存 Handler,后续通过 Handler 的名字就可以找到对应的 Handler,如果 Handler 的名字为空,则取默认的 Handler。

public void registerHandler(String handlerName, BridgeHandler handler) {
if (handler != null) {
// 添加至 Map<String, BridgeHandler>
messageHandlers.put(handlerName, handler);
}
}

public void setDefaultHandler(BridgeHandler handler) {
this.defaultHandler = handler;
}

然后,JS 端通过 callHandler 或 send 方法开始调用 Native:

function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}

function send(data, responseCallback) {
_doSend({
data: data
}, responseCallback);
}

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;
}

JS 端会生成一个唯一的 callbackId,用于保存 JS 回调函数,然后把消息添加到 JS 的消息队列,这个消息包含三个字段,分别是 handlerName、data 和 callbackId。最后通过刷新 iframe 的 src 属性 yy://__QUEUE_MESSAGE__/与 Native 通信。Native 拦截到这个 url,执行 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);
// 添加至 Map<String, CallBackFunction>
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}

这里实际上是执行了 _fetchQueue() 这个 JS 函数,同时保存 Native 的回调函数到 Map 对象,方便后续处理 _fetchQueue() 返给 Native 的数据。接下来看看 _fetchQueue() 这个 JS 函数:

function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
if (messageQueueString !== '[]') {
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
}

它从 JS 的消息队列获取消息,转换成 JSON 字符串,然后通过刷新 iframe 的 src 属性 yy://return/_fetchQueue/messageQueueString与 Native 通信,其中 messageQueueString 是消息列表的 JSON 字符串表示,示例如下:

[{"handlerName":"submitFromWeb","data":{"param":"test"},"callbackId":"cb_1_1552271280189"}]

Native 拦截到这个 url,执行 handlerReturnData(url)

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;
}
}

这里会从 url 中取出方法名和数据,再从 Map 里取出该方法名对应的 Native 回调函数,这里实际上就是 flushMessageQueue 方法里设置的匿名 CallBackFunction,它会把 JSON 格式的数据反序列化成消息列表,挨个处理消息。

@Override
public void onCallBack(String data) {
// 反序列化消息,处理消息
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();
if (!TextUtils.isEmpty(responseId)) {
// Native 调 JS 时,执行 Native 回调
......
} else {
// JS 调 Native 时,Native 处理 JS 传过来的数据
......
}
}
}

由于消息里只包含 handlerName、data、callbackId 这三个字段,没有 responseId,因此走的是 else 这个分支,具体代码如下:

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) {
// 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);
}

根据消息里的 handlerName 从 Map 里找到注册的 Handler,在 Handler 里处理 JS 传过来的 data,并执行 responseFunction.onCallBack(responseData) 将响应数据回给 JS,这里主要看一下 queueMessage 方法:

private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}

startupMessage 在 onPageFinished 会被设置为 null,如果其不为 null,说明页面还在加载中,把消息保存到消息列表,等页面加载完成时,再将消息逐个取出进行分发。分发消息执行 dispatchMessage方法:

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("(?<=[^\\\\])(\')", "\\\\\'");
messageJson = messageJson.replaceAll("%7B", URLEncoder.encode("%7B"));
messageJson = messageJson.replaceAll("%7D", URLEncoder.encode("%7D"));
messageJson = messageJson.replaceAll("%22", URLEncoder.encode("%22"));
String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
// 必须要找主线程才会将数据传递出去 --- 划重点
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
}
}

这里在分发消息时,实际上是执行了 _handleMessageFromNative() 这个 JS 函数,由于把消息转成 JSON 字符串了,所以在拼接要执行的 JS 时需要进行特殊字符转义。消息的 JSON 字符串示例如下:

{"responseData":"response data from Java","responseId":"cb_2_1552272760106"}

接下来看看 _handleMessageFromNative() 这个 JS 函数:

function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
}
_dispatchMessageFromNative(messageJSON);
}

function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
if (message.responseId) {
// JS 调 Native 时,执行 JS 回调
......
} else {
// Native 调 JS 时,JS 处理 Native 传过来的数据
......
}
});
}

receiveMessageQueue 在 JS 调用 init 方法时会被设置为 null,如果其不为 null,说明还没有调用 init 方法,在这种情况下,消息会被保存到 receiveMessageQueue,等到调用 init 时,再将消息再逐个取出进行分发。分发消息执行 dispatchMessage方法。由于消息里只包含 responseData 和 responseId 这两个字段,因此走的是 if 这个分支,具体代码如下:

responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];

根据 responseId 取出 JS 回调函数,然后执行 JS 回调。这个 responseId 实际就是最开始 JS 调 Native 时由 JS 生成的唯一的 callbackId,JS 将其传递给 Native 之后,Native 将其放到 responseId 字段里又传回给 JS。

至此,整个 JS 调 Native 的流程结束。详细流程如下图:

Native 调 JS 流程分析

首先,在 JS 端通过 registerHandler 方法注册一个特定 Handler,或者通过 init 方法注册一个默认的 Handler。

function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}

function init(messageHandler) {
if (WebViewJavascriptBridge._messageHandler) {
throw new Error('WebViewJavascriptBridge.init called twice');
}
WebViewJavascriptBridge._messageHandler = messageHandler;
var receivedMessages = receiveMessageQueue;
receiveMessageQueue = null;
for (var i = 0; i < receivedMessages.length; i++) {
_dispatchMessageFromNative(receivedMessages[i]);
}
}

然后,Native 端通过 callHandler 或 send 方法开始调用 JS。

public void callHandler(String handlerName, String data, CallBackFunction callBack) {
doSend(handlerName, data, callBack);
}

public void send(String data) {
send(data, null);
}

public void send(String data, CallBackFunction responseCallback) {
doSend(null, data, responseCallback);
}

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);
}

Native 端会生成一个唯一的 callbackId,用于保存 Native 回调函数。然后通过 queueMessage 方法分发消息,这个消息包含三个字段,分别是 handlerName、data 和 callbackId。消息 JSON 示例如下:

{"callbackId":"JAVA_CB_3_973", "data":"data from Java", "handlerName":"functionInJs"}

queueMessage 方法前面已经分析过,它实际上是执行了 _handleMessageFromNative() 这个 JS 函数。由于消息里不包含 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 传过来的数据,根据 handlerName 查找 JS 注册的对应的 Handler,然后通过 _doSend 方法将响应数据回给 Native,最后执行 Native 回调。

具体流程跟前面分析的 JS 调 Native 的流程基本一致,先往 JS 消息队列添加消息,然后通知 Native 端执行 flushMessageQueue 去刷新消息队列。只不过这里的消息只有 responseId 和 responseData 字段,而这个 responseId 实际就是最开始 Native 调 JS 时由 Native 生成的唯一的 callbackId,Native 将其传递给 JS 之后,JS 将其放到 responseId 字段里又传回给 Native,Native 根据 responseId 找到对应的回调函数,执行回调。

if (!TextUtils.isEmpty(responseId)) {
CallBackFunction function = responseCallbacks.get(responseId);
String responseData = m.getResponseData();
if (null != function) {
function.onCallBack(responseData);
}
responseCallbacks.remove(responseId);
}

至此,整个 Native 调 JS 的流程结束。详细流程如下图:

杂谈

JS 和 Native 的通信机制

JS 给 Native 发消息主要是向 body 中添加一个不可见的 iframe 元素,通过刷新 iframe 的 src 属性,然后就可以让 WebView 在 shouldOverrideUrlLoading 拦截到特殊格式的 url,从而获取到 JS 传递给 Native 的参数,执行不同的 Native 方法。同时,由于页面本身不跳转,用户无感知。

JS 给 Native 发消息主要分成两步,第一步先将消息添加到 JS 消息队列,然后发消息通知 Native 去刷新其消息队列,Native 通过 loadUrl 执行 JS 函数 _fetchQueue。第二步从消息队列一次性取出所有消息,再通过 url 的方式传递给 Native,Native 收到消息后一个一个串行处理。

Native 给 JS 发消息主要是通过 loadUrl 执行 JS。最主要的就是 _handleMessageFromNative, 将 Native 参数传递给 JS。

JS 和 Native 的实现逻辑

通过分析 JS 调 Native 以及 Native 调 JS 的流程,发现在 JS 端和 Native 端的实现非常类似,基本上是用 JavaScript 和 Java 实现了类似的逻辑。

例如在调用开始时,均需要生成一个唯一的回调 Id,用来保存本端的回调函数。

另外,在消息的处理逻辑上,两端也很类似,如果消息里包含 responseId 则执行本端的回调函数,否则处理另一端传过来的数据:根据 handlerName 查找指定的 Handler,将收到的 callbackId 原样存入 responseId 字段再回给另一端,以执行另一端的回调函数。

队列的应用

JS 和 Native 端的实现都使用了队列这个数据结构。

  • Native 消息队列

    Native 的 queueMessage 方法,当页面没有加载完成时,将消息先保存到队列,等页面加载完成时再挨个分发队列里的消息。这样可以避免在页面未加载完成时进行一些操作导致的异常。

  • JS 消息队列

    当 JS 端没有调用 init 方法时,Native 通过 send 方法给 JS 发消息时,消息并不会立即被处理,而是先保存到队列,等 JS 端调用 init 方法时,再创建默认的 Handler,然后挨个分发队列里的消息。

    当 JS 通过 _doSend 方法给 Native 发消息时,消息也是先加到 JS 消息队列里,然后再通知 Native 执行 JS,最后 一次性从 JS 消息队列里取出所有的消息传给 Native。