本文简单介绍了 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) { |
JS 调 Native
以下是一个例子,实现按下物理返回键时响应 WebView 的 JS 方法。
首先,在 Native 端通过 registerHandler 方法注册一个设置物理返回键监听的接口供 JS 调用,当按下物理返回键时执行 JS 回调:
webview.registerHandler("setPhysicalBackListener", new BridgeHandler() { |
然后,在 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( |
Native 调 JS
以下是一个例子,实现在 Native 端调用在 JS 中定义的 JS 函数。
首先,在 JS 端通过 registerHandler 方法注册一个 Handler:
connectWebViewJavascriptBridge(function(bridge) { |
然后,在 Native 端通过 callHandler 方法调用:
webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() { |
同样的,JSBridge 也提供了无方法名的调用方式。
首先需要在 JS 端设置一个默认的 Handler:
connectWebViewJavascriptBridge(function(bridge) { |
然后,在 Native 端通过 send 方法调用:
webView.send("hello"); |
源码解析
核心类分析
核心的 Java 类主要有两个:BridgeWebView
和 BridgeWebViewClient
。
另外,还有一个 WebViewJavascriptBridge.js
,主要的 JS 代码都在这个 JS 文件里。
先来看 BridgeWebView
的构造函数:
public BridgeWebView(Context context) { |
初始化时设置了一个自定义的 WebViewClient。
接下来看看 BridgeWebViewClient
这个类:
在 onPageFinished
加载 JS 文件 WebViewJavascriptBridge.js
,如果有未发送的消息,挨个取出进行分发。
|
在 shouldOverrideUrlLoading
方法拦截特殊格式的 url,执行 Native 代码。
|
下面着重分析一下 JS 和 Native 相互调用的流程。
JS 调 Native 流程分析
首先,在 Native 端通过 registerHandler 方法注册一个特定 Handler,或者通过 setdefaultHandler 方法注册一个默认 Handler。这里使用了一个 Map 对象保存 Handler,后续通过 Handler 的名字就可以找到对应的 Handler,如果 Handler 的名字为空,则取默认的 Handler。
public void registerHandler(String handlerName, BridgeHandler handler) { |
然后,JS 端通过 callHandler 或 send 方法开始调用 Native:
function callHandler(handlerName, data, responseCallback) { |
JS 端会生成一个唯一的 callbackId,用于保存 JS 回调函数,然后把消息添加到 JS 的消息队列,这个消息包含三个字段,分别是 handlerName、data 和 callbackId。最后通过刷新 iframe 的 src 属性 yy://__QUEUE_MESSAGE__/
与 Native 通信。Native 拦截到这个 url,执行 flushMessageQueue()
:
void flushMessageQueue() { |
这里实际上是执行了 _fetchQueue()
这个 JS 函数,同时保存 Native 的回调函数到 Map 对象,方便后续处理 _fetchQueue()
返给 Native 的数据。接下来看看 _fetchQueue()
这个 JS 函数:
function _fetchQueue() { |
它从 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) { |
这里会从 url 中取出方法名和数据,再从 Map 里取出该方法名对应的 Native 回调函数,这里实际上就是 flushMessageQueue
方法里设置的匿名 CallBackFunction,它会把 JSON 格式的数据反序列化成消息列表,挨个处理消息。
|
由于消息里只包含 handlerName、data、callbackId 这三个字段,没有 responseId,因此走的是 else 这个分支,具体代码如下:
CallBackFunction responseFunction = null; |
根据消息里的 handlerName 从 Map 里找到注册的 Handler,在 Handler 里处理 JS 传过来的 data,并执行 responseFunction.onCallBack(responseData) 将响应数据回给 JS,这里主要看一下 queueMessage
方法:
private void queueMessage(Message m) { |
startupMessage 在 onPageFinished 会被设置为 null,如果其不为 null,说明页面还在加载中,把消息保存到消息列表,等页面加载完成时,再将消息逐个取出进行分发。分发消息执行 dispatchMessage
方法:
void dispatchMessage(Message m) { |
这里在分发消息时,实际上是执行了 _handleMessageFromNative()
这个 JS 函数,由于把消息转成 JSON 字符串了,所以在拼接要执行的 JS 时需要进行特殊字符转义。消息的 JSON 字符串示例如下:
{"responseData":"response data from Java","responseId":"cb_2_1552272760106"} |
接下来看看 _handleMessageFromNative()
这个 JS 函数:
function _handleMessageFromNative(messageJSON) { |
receiveMessageQueue 在 JS 调用 init 方法时会被设置为 null,如果其不为 null,说明还没有调用 init 方法,在这种情况下,消息会被保存到 receiveMessageQueue,等到调用 init 时,再将消息再逐个取出进行分发。分发消息执行 dispatchMessage
方法。由于消息里只包含 responseData 和 responseId 这两个字段,因此走的是 if 这个分支,具体代码如下:
responseCallback = 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) { |
然后,Native 端通过 callHandler 或 send 方法开始调用 JS。
public void callHandler(String handlerName, String data, CallBackFunction callBack) { |
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) { |
以上 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)) { |
至此,整个 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。