DSBridge 源码分析

前言

目前公司所有 Web 容器都是使用 DSBridge 来实现原生与 JS 之间的通信,因此了解其原理非常重要,本文就来探究下其实现原理。

DSBridge-Android

DSBridge-iOS

分析

基本使用方式就不展开了,直接看看官方的文档即可。这里主要分析源码,首先查看下其文件结构:

DSBridge

其实现一共只有三个文件,而 CompletionHandler 以及 OnReturnValue 其实就是两个接口,通过使用前者将结果告知给前端,通过使用后者获取到前端返回的结果。其核心还是 DWebView,下面分 JS 调用 Native 、Native 调用 JS 以及 JS 端实现三部分进行分析。

JS 调用 Native

由于是 JS 主动调用 Native 应该只是注册监听,主要是看看哪里注册了,先从其构造方法开始看起。

public DWebView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DWebView(Context context) {
super(context);
init();
}

不管是代码中直接创建对象还是在布局文件中使用,都会调用 init 方法进行初始化。

private static final String BRIDGE_NAME = "_dsbridge";
private InnerJavascriptInterface innerJavascriptInterface = new InnerJavascriptInterface();
private void init() {
...
addInternalJavascriptObject(); // 1
super.addJavascriptInterface(innerJavascriptInterface, BRIDGE_NAME); // 4
}
private void addInternalJavascriptObject() {
addJavascriptObject(new Object() { // 2
@JavascriptInterface
public boolean hasNativeMethod(Object args) throws JSONException {}
@JavascriptInterface
public String closePage(Object object) throws JSONException {}
@JavascriptInterface
public void disableJavascriptDialogBlock(Object object) throws JSONException {}
@JavascriptInterface
public void dsinit(Object jsonObject) {}
@JavascriptInterface
public void returnValue(final Object obj) {}
}, "_dsb");
}
public void addJavascriptObject(Object object, String namespace) {
if (namespace == null) {
namespace = "";
}
if (object != null) {
javaScriptNamespaceInterfaces.put(namespace, object); // 3
}
}
private class InnerJavascriptInterface {
@JavascriptInterface
public String call(String methodName, String argStr) {
... // 4
}
}
  1. 添加内部的协议实现类其命名空间为 _dsb
  2. 该命名空间下一共有五个协议实现方法,作用如下。
    • hasNativeMethod JS 端可以调用该方法判断指定协议方法是否存在。
    • closePage JS 端可以调用该方法关闭当前容器,对于 Android 就是关闭当前 Activity。
    • disableJavascriptDialogBlock JS 可以调用该方法来禁止弹出 alert、confirm、prompt。
    • dsinit 当 JS 端初始化完毕后,会自动调用该方法,然后该方法内部执行所有在页面还未加载完前原生调用 JS 的方法。
    • returnValue 当原生调用 JS 端,JS 端需要返回给原生数据时调用该方法。
  3. 将该内部协议对象缓存到 javaScriptNamespaceInterfaces 以便后续取出反射调用方法。
  4. 通过调用 addJavascriptInterface 注册实际的协议对象。

看到这里 JS 调用 Native 的原理就已经很清楚了。首先 JS 通过调用 window._dsbridge.call 调用到 方法,然后方法内部根据命名空间进行转发即可,具体再看下 call 方法。

public String call(String methodName, String argStr) {
String[] nameStr = parseNamespace(methodName.trim()); // 1
Object jsb = javaScriptNamespaceInterfaces.get(nameStr[0]); // 2
methodName = nameStr[1];
JSONObject ret = new JSONObject();
ret.put("code", -1);
if (jsb == null) {
return ret.toString();
}
try {
JSONObject args = new JSONObject(argStr);
if (args.has("_dscbstub")) {
callback = args.getString("_dscbstub");
}
if (args.has("data")) {
arg = args.get("data");
}
} catch (JSONException e) {
return ret.toString();
}
Class<?> cls = jsb.getClass();
boolean asyn = false;
try {
method = cls.getMethod(methodName,
new Class[]{Object.class, CompletionHandler.class}); // 3
asyn = true;
} catch (Exception e) {
try {
method = cls.getMethod(methodName, new Class[]{Object.class}); // 4
} catch (Exception ex) {}
}
if (method == null) {
return ret.toString();
}
Object retData;
method.setAccessible(true);
try {
if (asyn) {
final String cb = callback;
method.invoke(jsb, arg, new CompletionHandler() { // 5
public void complete(Object retValue) {
complete(retValue, true);
}
public void complete() {
complete(null, true);
}
public void setProgressData(Object value) {
complete(value, false);
}
private void complete(Object retValue, boolean complete) { // 6
try {
JSONObject ret = new JSONObject();
ret.put("code", 0);
ret.put("data", retValue);
if (cb != null) {
String script = String.format("%s(%s.data);", cb, ret.toString());
if (complete) {
script += "delete window." + cb;
}
evaluateJavascript(script); // 7
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
} else { // 8
retData = method.invoke(jsb, arg);
ret.put("code", 0);
ret.put("data", retData);
return ret.toString();
}
} catch (Exception e) {
e.printStackTrace();
return ret.toString();
}
return ret.toString();
}
  1. methodName 拆分为命名空间以及对应的方法名,通过 . 号进行分割。
  2. javaScriptNamespaceInterfaces 中获取对应命名空间的协议实现类对象。
  3. 通过反射判断对应协议类是否存在异步实现方法,如果存在就使用该方法并标记为异步。
  4. 异步方法不存在那么再使用反射判断是否存在同步实现方法,如果存在就使用该方法,不存在那么返回错误。
  5. 如果是异步方法那么生成一个 CompletionHandler 实例,反射调用该异步方法。
  6. 异步方法实现中会调用 CompletionHandler.complete 将结果返回。
  7. 内部通过 evaluateJavascript 方法调用 JS 端对应的方法将结果进行传递。
  8. 如果是同步方法那么反射调用该同步方法获取到结果后就直接返回。

正如上面所说 InnerJavaScriptInterface.call 其实就是一个转发器,根据命名空间取出对应协议类实现,然后反射调用该方法,接着再将结果进行返回即可。

总结

具体 JS 端调用 Native 并获取到返回值流程如下图所示,从左到右 JS 端调用 Native 端,从右到左 Native 端将结果返回给 JS 端。

JSToNative

Native 调用 JS

由于是 Native 主动调用 JS 因此必定提供了一个方法,该方法就是 callHandler

public synchronized <T> void callHandler(String method, Object[] args, final OnReturnValue<T> handler) {
CallInfo callInfo = new CallInfo(method, ++callID, args); // 1
if (handler != null) {
handlerMap.put(callInfo.callbackId, handler); // 2
}
if (callInfoList != null) { // 3
callInfoList.add(callInfo);
} else {
dispatchJavascriptCall(callInfo); // 4
}
}
  1. 创建一个 callInfo 实例,该实例表示一次 Native 调用 JS,注意会传入一个 callID
  2. 如果存在 handler 实例(表示 Native 需要获取 JS 的返回值),那么将其将其缓存起来。
  3. 如果 callInfoList 不为空(表示 JS 端还未初始化完成)先缓存,初始化完后再调用。
  4. 如果 callInfoList 为空(表示 JS 端已经初始化完成),直接调用。

继续看下 dispatchJavascriptCall 的实现。

private void dispatchJavascriptCall(CallInfo info) {
evaluateJavascript(String.format("window._handleMessageFromNative(%s)", info.toString())); // 1
}
public void evaluateJavascript(final String script) {
runOnMainThread(new Runnable() { // 2
@Override
public void run() {
_evaluateJavascript(script);
}
});
}
private void _evaluateJavascript(String script) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // 3
DWebView.super.evaluateJavascript(script, null);
} else {
super.loadUrl("javascript:" + script); // 4
}
}
  1. 执行 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;
    }
    @Override
    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 拿到回调,再进行调用。

  2. 在主线程执行 JS 代码,如果不是主线程可能会抛出异常。

  3. 如果系统版本大于等于 4.4 那么调用 evaluateJavascript 调用 JS 如果不是那么使用 loadUrl 调用 JS。

问题来了 Native 调用了 JS ,那 JS 怎么把结果传递给 Native 呢?其实就是通过上面说过的默认协议的 returnValue 方法将结果告知 Native 的。

public void returnValue(final Object obj){
runOnMainThread(new Runnable() { // 1
@Override
public void run() {
JSONObject jsonObject = (JSONObject) obj;
Object data = null;
try {
int id = jsonObject.getInt("id"); // 2
boolean isCompleted = jsonObject.getBoolean("complete");
if (jsonObject.has("data")) { // 3
data = jsonObject.get("data");
}
OnReturnValue handler = handlerMap.get(id); // 4
if (handler != null) {
handler.onValue(data); // 5
if (isCompleted) {
handlerMap.remove(id);
}
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
  1. 主线程执行所有回调,外界不需要考虑线程。
  2. 获取到 callbackId 以及 complete,前者是回调标识,后者表示回调是否完成,如果是 false 表示有多次。
  3. 获取到 data 这个就是 JS 端 Native 返回的数据。
  4. 根据 callbackId 从缓存中获取到对应的回调对象。
  5. 调用 callback ,如果回调已经完成那么移除该回调。

总结

具体 Native 端调用 JS 并获取到返回值的流程如下图所示。

NativeToJS

JS 端实现

根据官方文档,前端需要引入这个 js 文件,其源码如下。

var bridge = {
default:this,
call: function(b, a, c) {
var e = "";
"function" == typeof a && (c = a, a = {});
a = {
data: void 0 === a ? null: a
};
if ("function" == typeof c) {
var g = "dscb" + window.dscb++;
window[g] = c;
a._dscbstub = g
}
a = JSON.stringify(a);
if (window._dsbridge) e = _dsbridge.call(b, a);
else if (window._dswk || -1 != navigator.userAgent.indexOf("_dsbridge")) e = prompt("_dsbridge=" + b, a);
return JSON.parse(e || "{}").data
},
register: function(b, a, c) {
c = c ? window._dsaf: window._dsf;
window._dsInit || (window._dsInit = !0, setTimeout(function() {
bridge.call("_dsb.dsinit")
}, 0));
"object" == typeof a ? c._obs[b] = a: c[b] = a
},
registerAsyn: function(b, a) {
this.register(b, a, !0)
},
hasNativeMethod: function(b, a) {
return this.call("_dsb.hasNativeMethod", {
name: b,
type: a || "all"
})
},
disableJavascriptDialogBlock: function(b) {
this.call("_dsb.disableJavascriptDialogBlock", {
disable: !1 !== b
})
}
};
!function() {
if (!window._dsf) {
var b = {
_dsf: {
_obs: {}
},
_dsaf: {
_obs: {}
},
dscb: 0,
dsBridge: bridge,
close: function() {
bridge.call("_dsb.closePage")
},
_handleMessageFromNative: function(a) {
var e = JSON.parse(a.data),
b = {
id: a.callbackId,
complete: !0
},
c = this._dsf[a.method],
d = this._dsaf[a.method],
h = function(a, c) {
b.data = a.apply(c, e);
bridge.call("_dsb.returnValue", b)
},
k = function(a, c) {
e.push(function(a, c) {
b.data = a;
b.complete = !1 !== c;
bridge.call("_dsb.returnValue", b)
});
a.apply(c, e)
};
if (c) h(c, this._dsf);
else if (d) k(d, this._dsaf);
else if (c = a.method.split("."), !(2 > c.length)) {
a = c.pop();
var c = c.join("."),
d = this._dsf._obs,
d = d[c] || {},
f = d[a];
f && "function" == typeof f ? h(f, d) : (d = this._dsaf._obs, d = d[c] || {},
(f = d[a]) && "function" == typeof f && k(f, d))
}
}
},
a;
for (a in b) window[a] = b[a];
bridge.register("_hasJavascriptMethod",
function(a, b) {
b = a.split(".");
if (2 > b.length) return ! (!_dsf[b] && !_dsaf[b]);
a = b.pop();
b = b.join(".");
return (b = _dsf._obs[b] || _dsaf._obs[b]) && !!b[a]
})
}
} ();

首先对于 !function 是会立即执行的,在其内部会创建一个 b 对象,接着遍历它将其所有的成员挂载到 window 上,接着调用 bridge.register 注册一个回调 _hasJavascriptMethod 用于 Native 判断 JS 是否支持指定协议方法,来看一下 bridge.register 的源码。

register: function(b, a, c) {
c = c ? window._dsaf: window._dsf; // 1
window._dsInit || (window._dsInit = !0, setTimeout(function() { // 2
bridge.call("_dsb.dsinit") // 4
}, 0));
"object" == typeof a ? c._obs[b] = a: c[b] = a // 3
}
  1. 刚刚方法调用时没传第三个参数因此 c 为 undefined 因此其值变为 window._dsf
  2. 最初 window._dsInitundefined 因此执行后续代码 window._dsInit = !0window._dsInit 赋值成 true,然后设置定时器,但不立即执行,需要等到方法调用完成。
  3. 刚刚方法调用时传入第二个参数为方法因此会将其赋值给 window._dsf._hasJavascriptMethod
  4. 接着执行 call 方法,传递参数为 _dsb_dsinit 其源码如下。
call: function(b, a, c) {
var e = "";
"function" == typeof a && (c = a, a = {});
a = { // 1
data: void 0 === a ? null: a
};
if ("function" == typeof c) { // 2
var g = "dscb" + window.dscb++;
window[g] = c;
a._dscbstub = g
}
a = JSON.stringify(a);
if (window._dsbridge) e = _dsbridge.call(b, a); // 3
else if (window._dswk || -1 != navigator.userAgent.indexOf("_dsbridge")) e = prompt("_dsbridge=" + b, a); // 4
return JSON.parse(e || "{}").data
}
  1. 刚刚方法调用时没传第二个参数因此 a{data: null}void 是一元操作符执行操作数固定返回 undefined
  2. 刚刚方法调用时没传第三个参数因此这块代码暂时不执行。
  3. window._dsbridge 如果有值,那么表示是 Android ,直接调用 call 方法,上面也说了实际会调用到原生的 InnerJavaScriptInterface.call 方法,然后解析命名空间发现其正好是内部协议的命名空间 _dsb 因此原生的 dsinit 方法就会被调用。
  4. window._dsbridge 如果没值,那么表示是 iOS ,需要借助于 prompt 方法进行通信,原生那么会拦截该方法做相应的处理。
  5. 将 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) {
// do something
});
call: function(b, a, c) {
var e = "";
"function" == typeof a && (c = a, a = {});
a = {
data: void 0 === a ? null: a
};
if ("function" == typeof c) {
var g = "dscb" + window.dscb++;
window[g] = c;
a._dscbstub = g
}
a = JSON.stringify(a);
if (window._dsbridge) e = _dsbridge.call(b, a);
else if (window._dswk || -1 != navigator.userAgent.indexOf("_dsbridge")) e = prompt("_dsbridge=" + b, a);
return JSON.parse(e || "{}").data
}

由于存在第三个参数,因此会将其保存到 window.dscbN 中其中 N 表示的是 callbackId 这跟 Native 其实是一样的明确表示一次调用,然后传递的参数变为 {data: "data", _dsbridge: dscbN} ,这样原生就可以取出这个回调方法名,然后在处理完成后执行 window.dscbN(result) 来调用回调方法,并将结果传递过来。

JS 注册同步方法

JS 端调用方法如下:

dsBridge.register('add', function(x, y){
return x + y;
})

这个其实和初始化时注册 _hasJavascriptMethod 是一样的,最终会将回调方法赋值给 window._dsf.add ,接着原生调用 JS ,实际调用的方法为 _handleMessageFromNative , 其源码如下。

_handleMessageFromNative: function(a) {
var e = JSON.parse(a.data), // 1
b = {
id: a.callbackId,
complete: !0
},
c = this._dsf[a.method], // 2
d = this._dsaf[a.method],
h = function(a, c) {
b.data = a.apply(c, e);
bridge.call("_dsb.returnValue", b) // 3
},
k = function(a, c) {
e.push(function(a, c) {
b.data = a;
b.complete = !1 !== c;
bridge.call("_dsb.returnValue", b)
});
a.apply(c, e)
};
if (c) h(c, this._dsf); // 2
else if (d) k(d, this._dsaf);
else if (c = a.method.split("."), !(2 > c.length)) {
a = c.pop();
var c = c.join("."),
d = this._dsf._obs,
d = d[c] || {},
f = d[a];
f && "function" == typeof f ? h(f, d) : (d = this._dsaf._obs, d = d[c] || {},
(f = d[a]) && "function" == typeof f && k(f, d))
}
}

根据前面 Native 端的分析,传递过来的参数为 method、callbackId 以及 data 三部分。

  1. 这里转换出的 e 是一个数组,Native 传递过来的是 JSONArray 的字符串表达式。
  2. 取出 JS 端注册的方法,根据全方法名(命名空间加真实方法名)。
  3. 如果注册方法存在,那么调用 h 方法,内部再通过 apply 调用回调方法,注意 e 中包含几个参数就会调用几个参数的回调。
  4. 将 JS 调用结果使用 call("_dsb.returnValue") 返回给 Native。

JS 注册异步方法

JS 端调用方法如下。

dsBridge.registerAsyn('add', function(x, y, callback){
callback(x + y)
})
registerAsyn: function(b, a) {
this.register(b, a, !0)
},
register: function(b, a, c) {
c = c ? window._dsaf: window._dsf; // 1
window._dsInit || (window._dsInit = !0, setTimeout(function() { // 2
bridge.call("_dsb.dsinit") // 4
}, 0));
"object" == typeof a ? c._obs[b] = a: c[b] = a // 3
}

注册逻辑与同步差不多,不过这次把方法赋值给了 window._dsaf 了,接着 _handleMessageFromNative 处理逻辑也发生了变化。

_handleMessageFromNative: function(a) {
var e = JSON.parse(a.data),
b = {
id: a.callbackId,
complete: !0
},
c = this._dsf[a.method],
d = this._dsaf[a.method], // 1
h = function(a, c) {
b.data = a.apply(c, e);
bridge.call("_dsb.returnValue", b)
},
k = function(a, c) { // 2
e.push(function(a, c) {
b.data = a;
b.complete = !1 !== c;
bridge.call("_dsb.returnValue", b)
});
a.apply(c, e)
};
if (c) h(c, this._dsf); // 2
else if (d) k(d, this._dsaf);
else if (c = a.method.split("."), !(2 > c.length)) {
a = c.pop();
var c = c.join("."),
d = this._dsf._obs,
d = d[c] || {},
f = d[a];
f && "function" == typeof f ? h(f, d) : (d = this._dsaf._obs, d = d[c] || {},
(f = d[a]) && "function" == typeof f && k(f, d))
}
}
  1. 取出 JS 端注册的方法,根据全方法名(命名空间加真实方法名)。
  2. 在参数中新增一个方法,然后调用 JS 的回调方法,当回调方法处理完毕后调用新增的方法将结果传递。
  3. 新增方法获取到结果后使用 call("_dsb.returnValue") 返回给 Native。

还有一种注册回调时不传递方法而是传递对象的,用的不多暂时忽略。

0%