背景
之前『Android WebView 和 JavaScript 交互』一文介绍了普遍 Hybrid 开发场景下 Android 和 JavaScript 的通信方式,相信绝大多数项目都是使用这种方式进行交互的,但是这种方案存在一些安全和性能问题。
而 JetPack 给我们带来的 WebKit 库提供了一种新的交互方案。
导入依赖
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
}
Android 向 JavaScript 发送消息
Android 端代码如下:
class MainActivity : AppCompatActivity() {
...
private fun postMessage(msg: String) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) {
WebViewCompat.postWebMessage(
binding.webview,
WebMessageCompat(msg),
HOST.toUri()
)
}
}
companion object {
const val HOST = "http://192.168.1.108:5500"
}
}
先判断 WebView 是否支持此方式发送消息,然后调用 WebViewCompat 的 postWebMessage() 发送,该函数接收 3 个参数,第一个是 WebView,不用赘述,第二个是消息的包装类 WebMessageCompat,我们传一个 String 类型的消息即可,第三个参数用于限制哪个站源可以接收我们的消息,一般情况下我们传自有的域名,不限制的话也可以使用通配符 "*" 代替。
在 H5 端使用以下方法监听:
<html>
<script type="text/javascript">
...
window.addEventListener("message", function (event) {
console.log("收到消息:", event.data)
}, false)
</script>
...
</html>
除了简单的 String 类型,它还支持字节流,这在文件传输中十分有用。
修改 Android 端代码如下:
class MainActivity : AppCompatActivity() {
...
private fun postArrayBuffer(arrayBuffer: ByteArray) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
WebViewCompat.postWebMessage(
binding.webview,
WebMessageCompat(arrayBuffer),
HOST.toUri()
)
}
}
}
companion object {
const val HOST = "http://192.168.1.108:5500"
}
}
需要注意,发送字节流需要判断是否支持 WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER。
假如传送一个图片,H5 端监听后可以解析:
<html>
<script type="text/javascript">
...
window.addEventListener("message", function (event) {
...
if (data instanceof ArrayBuffer) {
const blob = new Blob([data], { type: "image/png" });
const url = URL.createObjectURL(blob);
const img = new Image();
img.src = url;
document.body.appendChild(img);
}
}, false)
</script>
...
</html>
JavaScript 向 Android 端发送消息
Android 端设置监听:
class MainActivity : AppCompatActivity() {
...
private fun addWebMessageListener() {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(binding.webview, "Android", setOf(HOST)) { view, message, sourceOrigin, isMainFrame, replyProxy ->
Log.e("moji", "Android 端收到消息:${message.data}")
// 收到消息后可以回复
replyProxy.postMessage("got the message @ ${System.currentTimeMillis()}")
}
}
}
companion object {
const val HOST = "http://192.168.1.108:5500"
}
}
首先同样需要判断是否支持监听,然后调用 WebViewCompat 的 addWebMessageListener() 方法添加监听,该函数接收 4 个参数,第一个是 WebView,不用赘述,第二个是注入 JS 对象的名称,这里命名为 Android,下面会用到,第三个参数用来指定哪些站源会注入这个对象,第四个参数就是回调接口。
收到消息我们可以调用 JavaScriptReplyProxy 对象的 postMessage() 给 H5 端发个回复。
然后 H5 端就可以发送消息了:
<html>
<script type="text/javascript">
...
function postMessage(message) {
if (typeof Android !== "undefined") {
Android.postMessage(message);
// 接收来自客户端的回复
Android.onmessage = function (event) {
console.log("Android.onmessage received: ", event);
}
}
}
</script>
...
</html>
其中 Android 就是上方我们注入对象的名称,调用 postMessage() 发送消息即可,假如 Android 端提供了回复,那么可以用 onmessage 来监听。
同样支持字节流,Android 端修改如下:
class MainActivity : AppCompatActivity() {
...
private fun addWebMessageListener() {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(binding.webview, "Android", setOf(HOST)) { view, message, sourceOrigin, isMainFrame, replyProxy ->
if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
val imageData = message.arrayBuffer
...
}
}
}
}
companion object {
const val HOST = "http://192.168.1.108:5500"
}
}
通过 getType() 判断如果是字节流,就直接从 WebMessageCompat 中取出数据即可。
H5 端使用同样的方式发送字节流就可以:
<html>
<script type="text/javascript">
...
function postArrayBuffer(buffer) {
if (typeof Android !== "undefined") {
Android.postMessage(buffer);
}
}
</script>
...
</html>
总结
这种方式安全,但是需要判断是否支持(遗憾的是国内大多数系统 WebView 升级进度拖沓),一来比较繁琐,二来在不支持的设备上仍要寻求其他解决方案,增加维护成本。
不过目前其他开发平台比如 iOS、HarmonyOS、Flutter 等亦采用类似的方案进行通信,不难看出这类方案将会成为主流。
