JSBridge源码分析

最近在研究Hybrid的相关东西,其中重要一项就是JS与Java的相互调用以及传递参数,传统的方法是通过Webview的addJavascriptInterface方法来注入一个JS全局对象,但这样容易产生很大的安全隐患。比较好的方法就是在加载网页的过程中动态注入一段JS脚本,JSBridge这个开源库就是利用此种方法,此库支持异步回调,方法参数支持js所有已知的类型,包括number、string、boolean、object、function;Java层方法可以返回void或能转为字符串的类型(如int、long、String、double、float等)或可序列化的自定义类型。下面将仔细分析它的工作原理。

脚本注入时机

在WebChromeClient的onProgressChanged方法中注入JS脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
   
@Override
public void onProgressChanged (WebView view, int newProgress) {
//为什么要在这里注入JS
//1 OnPageStarted中注入有可能全局注入不成功,导致页面脚本上所有接口任何时候都不可用
//2 OnPageFinished中注入,虽然最后都会全局注入成功,但是完成时间有可能太晚,当页面在初始化调用接口函数时会等待时间过长
//3 在进度变化时注入,刚好可以在上面两个问题中得到一个折中处理
//为什么是进度大于25%才进行注入,因为从测试看来只有进度大于这个数字页面才真正得到框架刷新加载,保证100%注入成功
if (newProgress <= 25) {
mIsInjectedJS = false;
} else if (!mIsInjectedJS) {
view.loadUrl(mJsCallJava.getPreloadInterfaceJS());
mIsInjectedJS = true;
Log.d(TAG, " inject js interface completely on progress " + newProgress);

}
super.onProgressChanged(view, newProgress);
}
````


# 如何生成脚本(重点)


```java

public JsCallJava (String injectedName, Class injectedCls) {
try {
if (TextUtils.isEmpty(injectedName)) {
throw new Exception("injected name can not be null");
}
mInjectedName = injectedName;
mMethodsMap = new HashMap<String, Method>();
//获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法
Method[] methods = injectedCls.getDeclaredMethods();
StringBuilder sb = new StringBuilder("javascript:(function(b){console.log(\"");
sb.append(mInjectedName);
sb.append(" initialization begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};");
for (Method method : methods) {
String sign;
if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(method)) == null) {
continue;
}
mMethodsMap.put(sign, method);
sb.append(String.format("a.%s=", method.getName()));
}

sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\"");
sb.append(mInjectedName);
sb.append(" call error, message:miss method name\"}var e=[];for(var h=1;h<f.length;h++){var c=f[h];var j=typeof c;e[e.length]=j;if(j==\"function\"){var d=a.queue.length;a.queue[d]=c;f[h]=d}}var g=JSON.parse(prompt(JSON.stringify({method:f.shift(),types:e,args:f})));if(g.code!=200){throw\"");
sb.append(mInjectedName);
sb.append(" call error, code:\"+g.code+\", message:\"+g.result}return g.result};Object.getOwnPropertyNames(a).forEach(function(d){var c=a[d];if(typeof c===\"function\"&&d!==\"callback\"){a[d]=function(){return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))}}});b.");
sb.append(mInjectedName);
sb.append("=a;console.log(\"");
sb.append(mInjectedName);
sb.append(" initialization end\")})(window);");
mPreloadInterfaceJS = sb.toString();
} catch(Exception e){
Log.e(TAG, "init js error:" + e.getMessage());
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

private String genJavaMethodSign (Method method) {
String sign = method.getName();
Class[] argsTypes = method.getParameterTypes();
int len = argsTypes.length;
if (len < 1 || argsTypes[0] != WebView.class) {
Log.w(TAG, "method(" + sign + ") must use webview to be first parameter, will be pass");
return null;
}
for (int k = 1; k < len; k++) {
Class cls = argsTypes[k];
if (cls == String.class) {
sign += "_S";
} else if (cls == int.class ||
cls == long.class ||
cls == float.class ||
cls == double.class) {
sign += "_N";
} else if (cls == boolean.class) {
sign += "_B";
} else if (cls == JSONObject.class) {
sign += "_O";
} else if (cls == JsCallback.class) {
sign += "_F";
} else {
sign += "_P";
}
}
return sign;
}

通过JsCallJava这个类的构造函数来实现,传入一个JS对象名以及一个JS要调用方法的类名,注意这个类的所有方法必须是public static。通过反射此注入类的所有静态方法,并把方法的名称和形参组合起来,一起作为key存到一个map中,map的value就是该静态方法。从代码也可以看出形参不区分整型、长整型、浮点类型等,因为在JS中统统都是number。
另外,JS是通过动态拼接起来,这样应该没人看得懂的,通过调试,把他还原如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
(function(b){

console.log("HostApp initialization begin");
var a={queue:[],callback:function(){
//得到arguments中的所有数组成员
var d=Array.prototype.slice.call(arguments,0);
//把数组的第一个元素从其中删除,并返回第一个元素的值。
var c=d.shift(); //数组索引
var e=d.shift(); //是否需要保留此function,
this.queue[c].apply(this,d);
//默认不持久callback,用完就删除
if(!e){delete this.queue[c]}}
};
//反射java层的所有函数
a.aabc=a.alert=a.alert=a.alert=a.delayJsCallBack=a.getIMSI=a.getOsSdk=
a.goBack=a.overloadMethod=a.overloadMethod=a.passJson2Java=a.passLongType=
a.retBackPassJson=a.retJavaObject=a.testLossTime=a.toast=a.toast=function(){
var f=Array.prototype.slice.call(arguments,0);
if(f.length<1){
throw"HostApp call error, message:miss method name"
}
var e=[];
//获取参数类型, 注意第0个是方法名,需要跳过
for(var h=1;h<f.length;h++) {
var c=f[h];
var j=typeof c;
e[e.length]=j;
//如果是回调函数,则给queue赋值,并返回queue中的索引
if(j=="function"){
var d=a.queue.length;
a.queue[d]=c;
f[h]=d
}
}
//调用Java方法
var g=JSON.parse(prompt(JSON.stringify({
method:f.shift(),
types:e,
args:f
}
)));
if(g.code!=200){
throw"HostApp call error, code:"+g.code+", message:"+g.result
}
//只返回result,和java层对应
return g.result
};
//遍历a对象的所有元素
Object.getOwnPropertyNames(a).forEach(
function(d){
var c=a[d];
if(typeof c==="function"&&d!=="callback"){
a[d]=function(){
//拼接一个数组,把方法名和参数拼接起来
return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))
}
}
});


b.HostApp=a;
console.log("HostApp initialization end")
}

)(window);

相关的地方本人都写了详细注释,应该没什么问题。JS调用Java是通过prompt方法来实现,参数为3个,第一个是要调用的Java注入类的方法名;第二个是传递的参数类型数组,第三个是要传递的参数数组。

ps:对于异步回调,做了特殊处理,其参数值是a对象中queue数组的索引,queue数组中存放的就是回调函数。

Webview处理prompt方法

在WebChromeClient的onJsPrompt方法处理prompt方法

1
2
3
4
5
6
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(mJsCallJava.call(view, message));
return true;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

public String call(WebView webView, String jsonStr) {
if (!TextUtils.isEmpty(jsonStr)) {
try {
JSONObject callJson = new JSONObject(jsonStr);
String methodName = callJson.getString("method");
JSONArray argsTypes = callJson.getJSONArray("types");
JSONArray argsVals = callJson.getJSONArray("args");
String sign = methodName;
int len = argsTypes.length();
Object[] values = new Object[len + 1];
int numIndex = 0;
String currType;

values[0] = webView;

for (int k = 0; k < len; k++) {
currType = argsTypes.optString(k);
if ("string".equals(currType)) {
sign += "_S";
values[k + 1] = argsVals.isNull(k) ? null : argsVals.getString(k);
} else if ("number".equals(currType)) {
sign += "_N";
numIndex = numIndex * 10 + k + 1;
} else if ("boolean".equals(currType)) {
sign += "_B";
values[k + 1] = argsVals.getBoolean(k);
} else if ("object".equals(currType)) {
sign += "_O";
values[k + 1] = argsVals.isNull(k) ? null : argsVals.getJSONObject(k);
} else if ("function".equals(currType)) {
sign += "_F";
values[k + 1] = new JsCallback(webView, mInjectedName, argsVals.getInt(k));
} else {
sign += "_P";
}
}

Method currMethod = mMethodsMap.get(sign);

// 方法匹配失败
if (currMethod == null) {
return getReturn(jsonStr, 500, "not found method(" + sign + ") with valid parameters");
}
// 数字类型细分匹配
if (numIndex > 0) {
Class[] methodTypes = currMethod.getParameterTypes();
int currIndex;
Class currCls;
while (numIndex > 0) {
currIndex = numIndex - numIndex / 10 * 10;
currCls = methodTypes[currIndex];
if (currCls == int.class) {
values[currIndex] = argsVals.getInt(currIndex - 1);
} else if (currCls == long.class) {
//WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number
values[currIndex] = Long.parseLong(argsVals.getString(currIndex - 1));
} else {
values[currIndex] = argsVals.getDouble(currIndex - 1);
}
numIndex /= 10;
}
}

return getReturn(jsonStr, 200, currMethod.invoke(null, values));
} catch (Exception e) {
//优先返回详细的错误信息
if (e.getCause() != null) {
return getReturn(jsonStr, 500, "method execute error:" + e.getCause().getMessage());
}
return getReturn(jsonStr, 500, "method execute error:" + e.getMessage());
}
} else {
return getReturn(jsonStr, 500, "call data empty");
}
}

将prompt的json参数传递到了call方法中,然后就能得到相应的方法名,参数类型,参数值;通过方法名和参数类型就能从注入类找到相应方法,最后通过参数值构造实际参数,反射调用注入类的方法即可完成整个JS->Java的调用。

JS异步回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

private static final String CALLBACK_JS_FORMAT = "javascript:%s.callback(%d, %d %s);";
public void apply (Object... args) throws JsCallbackException {
if (mWebViewRef.get() == null) {
throw new JsCallbackException("the WebView related to the JsCallback has been recycled");
}
if (!mCouldGoOn) {
throw new JsCallbackException("the JsCallback isn't permanent,cannot be called more than once");
}
StringBuilder sb = new StringBuilder();
for (Object arg : args){
sb.append(",");
boolean isStrArg = arg instanceof String;
if (isStrArg) {
sb.append("\"");
}
sb.append(String.valueOf(arg));
if (isStrArg) {
sb.append("\"");
}
}
String execJs = String.format(CALLBACK_JS_FORMAT, mInjectedName, mIndex, mIsPermanent, sb.toString());
Log.d("JsCallBack", execJs);
mWebViewRef.get().loadUrl(execJs);
mCouldGoOn = mIsPermanent > 0;
}


异步回调是通过JsCallback的apply来进行的,具体实现就是上面分析的JS。

1
2
3
4
5
var d=Array.prototype.slice.call(arguments,0);
var c=d.shift();
var e=d.shift();
this.queue[c].apply(this,d);
if(!e){delete this.queue[c]}}

ps:回调只能使用一次,用完就会被删除,如果要多次使用,可以使用JsCallback提供setPermanent(true)来实现。