前言
目前公司所有 Web 容器都是使用 DSBridge 来实现原生与 JS 之间的通信,因此了解其原理非常重要,本文就来探究下其实现原理。
分析
基本使用方式就不展开了,直接看看官方的文档即可。这里主要分析源码,首先查看下其文件结构:
其实现一共只有三个文件,而 CompletionHandler 以及 OnReturnValue 其实就是两个接口,通过使用前者将结果告知给前端,通过使用后者获取到前端返回的结果。其核心还是 DWebView,下面分 JS 调用 Native 、Native 调用 JS 以及 JS 端实现三部分进行分析。
JS 调用 Native
由于是 JS 主动调用 Native 应该只是注册监听,主要是看看哪里注册了,先从其构造方法开始看起。
public DWebView(Context context, AttributeSet attrs) { |
不管是代码中直接创建对象还是在布局文件中使用,都会调用 init 方法进行初始化。
private static final String BRIDGE_NAME = "_dsbridge"; |
- 添加内部的协议实现类其命名空间为
_dsb
。 - 该命名空间下一共有五个协议实现方法,作用如下。
hasNativeMethod
JS 端可以调用该方法判断指定协议方法是否存在。closePage
JS 端可以调用该方法关闭当前容器,对于 Android 就是关闭当前 Activity。disableJavascriptDialogBlock
JS 可以调用该方法来禁止弹出 alert、confirm、prompt。dsinit
当 JS 端初始化完毕后,会自动调用该方法,然后该方法内部执行所有在页面还未加载完前原生调用 JS 的方法。returnValue
当原生调用 JS 端,JS 端需要返回给原生数据时调用该方法。
- 将该内部协议对象缓存到
javaScriptNamespaceInterfaces
以便后续取出反射调用方法。 - 通过调用
addJavascriptInterface
注册实际的协议对象。
看到这里 JS 调用 Native 的原理就已经很清楚了。首先 JS 通过调用 window._dsbridge.call
调用到 方法,然后方法内部根据命名空间进行转发即可,具体再看下 call
方法。
public String call(String methodName, String argStr) { |
- 将
methodName
拆分为命名空间以及对应的方法名,通过 . 号进行分割。 - 从
javaScriptNamespaceInterfaces
中获取对应命名空间的协议实现类对象。 - 通过反射判断对应协议类是否存在异步实现方法,如果存在就使用该方法并标记为异步。
- 异步方法不存在那么再使用反射判断是否存在同步实现方法,如果存在就使用该方法,不存在那么返回错误。
- 如果是异步方法那么生成一个
CompletionHandler
实例,反射调用该异步方法。 - 异步方法实现中会调用
CompletionHandler.complete
将结果返回。 - 内部通过
evaluateJavascript
方法调用 JS 端对应的方法将结果进行传递。 - 如果是同步方法那么反射调用该同步方法获取到结果后就直接返回。
正如上面所说 InnerJavaScriptInterface.call
其实就是一个转发器,根据命名空间取出对应协议类实现,然后反射调用该方法,接着再将结果进行返回即可。
总结
具体 JS 端调用 Native 并获取到返回值流程如下图所示,从左到右 JS 端调用 Native 端,从右到左 Native 端将结果返回给 JS 端。
Native 调用 JS
由于是 Native 主动调用 JS 因此必定提供了一个方法,该方法就是 callHandler
。
public synchronized <T> void callHandler(String method, Object[] args, final OnReturnValue<T> handler) { |
- 创建一个
callInfo
实例,该实例表示一次 Native 调用 JS,注意会传入一个 callID。 - 如果存在
handler
实例(表示 Native 需要获取 JS 的返回值),那么将其将其缓存起来。 - 如果
callInfoList
不为空(表示 JS 端还未初始化完成)先缓存,初始化完后再调用。 - 如果
callInfoList
为空(表示 JS 端已经初始化完成),直接调用。
继续看下 dispatchJavascriptCall
的实现。
private void dispatchJavascriptCall(CallInfo info) { |
执行 JS 代码
window_handleMessageFromNative()
其实也就是调用了一个方法参数传递如下。private static class CallInfo {
private String data;
private int callbackId;
private String method;
CallInfo(String handlerName, int id, Object[] args) {
if (args == null) args = new Object[0];
data = new JSONArray(Arrays.asList(args)).toString();
callbackId = id;
method = handlerName;
}
public String toString() {
JSONObject jo = new JSONObject();
try {
jo.put("method", method);
jo.put("callbackId", callbackId);
jo.put("data", data);
} catch (JSONException e) {
e.printStackTrace();
}
return jo.toString();
}
}传递的参数包括方法名、方法参数以及 callbackId,上面说过如果有回调就会先进行缓存,而缓存的 key 就是这个 callbackId,这里可以猜想后续拿到 JS 端返回的结果后就通过 callbackId 拿到回调,再进行调用。
在主线程执行 JS 代码,如果不是主线程可能会抛出异常。
如果系统版本大于等于 4.4 那么调用
evaluateJavascript
调用 JS 如果不是那么使用loadUrl
调用 JS。
问题来了 Native 调用了 JS ,那 JS 怎么把结果传递给 Native 呢?其实就是通过上面说过的默认协议的 returnValue
方法将结果告知 Native 的。
public void returnValue(final Object obj){ |
- 主线程执行所有回调,外界不需要考虑线程。
- 获取到 callbackId 以及 complete,前者是回调标识,后者表示回调是否完成,如果是 false 表示有多次。
- 获取到 data 这个就是 JS 端 Native 返回的数据。
- 根据 callbackId 从缓存中获取到对应的回调对象。
- 调用 callback ,如果回调已经完成那么移除该回调。
总结
具体 Native 端调用 JS 并获取到返回值的流程如下图所示。
JS 端实现
根据官方文档,前端需要引入这个 js 文件,其源码如下。
var bridge = { |
首先对于 !function
是会立即执行的,在其内部会创建一个 b
对象,接着遍历它将其所有的成员挂载到 window
上,接着调用 bridge.register
注册一个回调 _hasJavascriptMethod
用于 Native 判断 JS 是否支持指定协议方法,来看一下 bridge.register
的源码。
register: function(b, a, c) { |
- 刚刚方法调用时没传第三个参数因此
c
为 undefined 因此其值变为window._dsf
。 - 最初
window._dsInit
为undefined
因此执行后续代码window._dsInit = !0
将window._dsInit
赋值成 true,然后设置定时器,但不立即执行,需要等到方法调用完成。 - 刚刚方法调用时传入第二个参数为方法因此会将其赋值给
window._dsf._hasJavascriptMethod
。 - 接着执行
call
方法,传递参数为_dsb_dsinit
其源码如下。
call: function(b, a, c) { |
- 刚刚方法调用时没传第二个参数因此
a
为{data: null}
,void
是一元操作符执行操作数固定返回undefined
。 - 刚刚方法调用时没传第三个参数因此这块代码暂时不执行。
window._dsbridge
如果有值,那么表示是 Android ,直接调用call
方法,上面也说了实际会调用到原生的InnerJavaScriptInterface.call
方法,然后解析命名空间发现其正好是内部协议的命名空间_dsb
因此原生的dsinit
方法就会被调用。window._dsbridge
如果没值,那么表示是 iOS ,需要借助于prompt
方法进行通信,原生那么会拦截该方法做相应的处理。- 将 Native 的返回结果返回给外界。
至此 JS 的初始化工作完成了,Native 的 dsinit
方法也被调用了,下面分析几个实际应用场景。
JS 同步调用 Native
JS 端调用方法如下:
const result = dsBridge.call("nameSpace.methodName", "data"); |
实际上就跟 dsinit
一模一样只是原生收到的第二个参数变成了 {data: "data"}
。
JS 异步调用 Native
JS 端调用方法如下:
dsBridge.call("nameSpace.methodName", "data", function(result) { |
由于存在第三个参数,因此会将其保存到 window.dscbN
中其中 N 表示的是 callbackId 这跟 Native 其实是一样的明确表示一次调用,然后传递的参数变为 {data: "data", _dsbridge: dscbN}
,这样原生就可以取出这个回调方法名,然后在处理完成后执行 window.dscbN(result)
来调用回调方法,并将结果传递过来。
JS 注册同步方法
JS 端调用方法如下:
dsBridge.register('add', function(x, y){ |
这个其实和初始化时注册 _hasJavascriptMethod
是一样的,最终会将回调方法赋值给 window._dsf.add
,接着原生调用 JS ,实际调用的方法为 _handleMessageFromNative
, 其源码如下。
_handleMessageFromNative: function(a) { |
根据前面 Native 端的分析,传递过来的参数为 method、callbackId 以及 data 三部分。
- 这里转换出的
e
是一个数组,Native 传递过来的是 JSONArray 的字符串表达式。 - 取出 JS 端注册的方法,根据全方法名(命名空间加真实方法名)。
- 如果注册方法存在,那么调用
h
方法,内部再通过apply
调用回调方法,注意e
中包含几个参数就会调用几个参数的回调。 - 将 JS 调用结果使用
call("_dsb.returnValue")
返回给 Native。
JS 注册异步方法
JS 端调用方法如下。
dsBridge.registerAsyn('add', function(x, y, callback){ |
注册逻辑与同步差不多,不过这次把方法赋值给了 window._dsaf
了,接着 _handleMessageFromNative
处理逻辑也发生了变化。
_handleMessageFromNative: function(a) { |
- 取出 JS 端注册的方法,根据全方法名(命名空间加真实方法名)。
- 在参数中新增一个方法,然后调用 JS 的回调方法,当回调方法处理完毕后调用新增的方法将结果传递。
- 新增方法获取到结果后使用
call("_dsb.returnValue")
返回给 Native。
还有一种注册回调时不传递方法而是传递对象的,用的不多暂时忽略。