背景

之前『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 是否支持此方式发送消息,然后调用 WebViewCompatpostWebMessage() 发送,该函数接收 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"
    }
}

首先同样需要判断是否支持监听,然后调用 WebViewCompataddWebMessageListener() 方法添加监听,该函数接收 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 等亦采用类似的方案进行通信,不难看出这类方案将会成为主流。