1. 1. 1.配置 WebView,打开 JS 和 Java 的通信
  2. 2. 2.InjectedChromeClient.java
    1. 2.1. 2.1在构造方法中构造JsCallJava,生成待注入的JS代码
    2. 2.2. 2.2 注入JS
    3. 2.3. 2.3 监听前端调用 Js Prompt 接口
  3. 3. 3.JsCallJava.java
    1. 3.1. 3.1 Call
  4. 4. 4.JsCallback.java
    1. 4.1. 4.1 异步回调Java

目前混合应用开发有两种套路:
一种是 ReactNative 这样的完整解决方案,开发方式完全不同了,需要投入较大学习成本。而且ReactNative推出也不算久,一直还在迭代,不知道会不会出什么问题;
另一种就是自己做 Web 前端,自己打开一个 JS 与 Java 的交互通道,遇到什么需求自己一路趟平过去。
而可惜的是, 对于 JS 与 Java 交互,系统提供的 addjavascriptInterface 在安卓 4.2 以下的系统中爆出了极高风险的漏洞,基本等于废的。比较流行的替代方案是 Safe Java-JS WebView Bridge。此文是对Safe Java-JS WebView Bridge的源码解析。

1.配置 WebView,打开 JS 和 Java 的通信

1
2
3
4
5
6
7
WebSettings ws = wv.getSettings();
ws.setJavaScriptEnabled(true);
wv.setWebChromeClient(
//CustomChromeClient继承自InjectedChromeClient,参见下一节分析
new CustomChromeClient("HostApp", HostJsScope.class)
);
wv.loadUrl("file:///android_asset/test.html");

wv.setWebChromeClient 使WebView能够处理JS,favicons, titles, 和progress等;WebView还有一个名字很像的方法setWebViewClient 。它们的区别是WebChromeClient用来处理Web网页发出的一些事件,而WebViewClient处理WebView的事件。可以对比它们一些方法感受下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WebChromeClient 
————————————————

onCloseWindow(关闭WebView)

onCreateWindow()

onJsAlert (WebView上alert 是弹不出来东西的,需要定制你的 WebChromeClient 处理弹出)

onJsPrompt

onJsConfirm

onProgressChanged

onReceivedIcon

onReceivedTitle
1
2
3
4
5
6
7
8
9
WebViewClient 
——————————————
onLoadResource

onPageStart

onPageFinish

onReceiveError

2.InjectedChromeClient.java

继承自WebChromeClient,所谓的 Js 与 Java 的通讯 Bridge 其实就是通过 WebChromeClient 去监听 onJsPrompt 实现的。之所以用 Prompt 而不用 Alert 和 Confirm 是因为 Prompt 的使用概率比较低。在 js 中,alert 和 confirm 的使用概率是很高的。

2.1在构造方法中构造JsCallJava,生成待注入的JS代码

1
2
3
public InjectedChromeClient (String injectedName, Class injectedCls) {
mJsCallJava = new JsCallJava(injectedName, injectedCls);
}

生成的JavaScript代码例子

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
javascript:(function (b) {
console.log("HostApp 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]
}
}
};
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 = [];
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 "HostApp 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.HostApp = a;
console.log("HostApp initialization end")
})(window);

2.2 注入JS

在onProgressChanged中进行注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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);
}

2.3 监听前端调用 Js Prompt 接口

在前端调用HostJsScope对应的Js接口,发出webview的onPrompt事件,进而调用本地HostJsScope方法,如果是同步且有返回值,通过prompt返回值返回,如果是异步,则反射调用JsCallback将数据返回前端。

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

3.JsCallJava.java

3.1 Call

将生成JS代码的方法逆向,解析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
67
68
69
70
71
72
73
74
75
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");
}
}

4.JsCallback.java

4.1 异步回调Java

生成JS用WebView再loadUrl一次,主动调用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
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;
}