本页将探讨用于建立原生桥(也称为 JavaScript 桥)的各种方法和最佳实践,以促进 WebView 中的 Web 内容与宿主 Android 应用之间的通信。
这使 Web 开发者能够使用 JavaScript 访问标准 Web API 通常不提供的原生平台功能,例如摄像头、文件系统或高级硬件传感器。
用例
JavaScript 桥接实现支持各种集成方案,在这些方案中,Web 内容需要更深入地访问 Android 操作系统。下面列出了一些示例:
- 平台集成:从网页触发原生 Android 界面组件(例如,生物识别提示、
BottomSheetDialog)。 - 性能:将繁重的计算任务分流到原生 Java 或 Kotlin 代码。
- 数据持久性:访问本地加密数据库或共享偏好设置。
- 大数据传输:在应用和 Web 渲染器之间传递媒体文件或复杂的数据结构。
通信机制
Android 提供三代主要 API 来建立原生桥接。 虽然这些方法仍然可用,但在安全性、易用性和性能方面存在显著差异。
使用 addWebMessageListener(推荐)
addWebMessageListener 是网页内容与原生应用代码之间最现代且推荐的通信方法。它将 JavaScript 界面的易用性与消息传递系统的安全性相结合。
工作原理:应用添加一个具有特定名称和一组允许的来源规则的监听器。然后,WebView 会确保 JavaScript 对象从网页开始加载的那一刻起就存在于全局范围 (window.objectName) 中。
初始化:为确保 WebView 在任何脚本运行之前注入 JavaScript 对象,您必须先调用 addWebMessageListener,然后再调用 loadUrl()。
主要功能:
安全性和信任:与旧版 API 不同,此方法在初始化期间需要
Set<String>的allowedOriginRules。这是建立信任的主要机制。指定可信来源(例如
https://example.com)后,WebView 会保证仅向从该确切来源加载的网页公开注入的 JavaScript 对象。原生监听器回调会在每次收到消息时接收
sourceOrigin参数。如果您的网桥支持多个允许的来源,您可以使用此方法来验证发件人的确切来源。由于 WebView 在平台级严格执行这些来源检查,因此您的应用通常可以信任从可信
sourceOrigin收到的消息,从而无需在大多数标准实现中进行严格的载荷验证。- WebView 会根据架构 (HTTP/HTTPS)、主机和端口来匹配规则。
- WebView 会忽略路径。例如,
https://example.com允许https://example.com/login和https://example.com/home。 - WebView 严格限制通配符只能用于子网域的主机开头。例如,
https://*.example.com与https://foo.example.com匹配,但与https://example.com不匹配。如果您需要同时匹配https://example.com及其子网域,则必须将每个来源规则分别添加到许可名单中(例如"https://example.com", "https://*.example.com")。您不能在方案中使用通配符,也不能在网域中间使用通配符。
这会将网桥限制为仅限经过验证的网域,从而防止未经授权的第三方内容或注入的 iframe 执行原生代码。
多框架支持:适用于符合来源规则的所有框架。
线程:监听器回调在应用的主线程(界面线程)上运行。如果您的网桥需要处理复杂的数据处理、JSON 解析或数据库查找,您必须将该工作分流到后台线程,以防止应用界面因“应用无响应”(ANR) 错误而冻结。
双向:当网页发送消息时,应用会收到一个
JavaScriptReplyProxy,该对象可用于将消息发送回该特定框架。您可以保留此replyProxy对象,并随时使用它向相应 Facebook 页面发送任意数量的消息,而不仅仅是回复该页面发送的每条消息。如果原始框架离开或被销毁,则使用代理上的postMessage()发送的消息会被静默忽略。应用端启动:虽然网页必须始终启动与应用的通信渠道,但原生应用可以单方面提示网页开始此流程。原生应用可以使用
addDocumentStartJavaScript()(在网页加载之前评估 JavaScript)或evaluateJavaScript()(在网页加载之后评估 JavaScript)与网页通信。
限制:此 API 以字符串或 byte[] 数组的形式发送数据。对于更复杂的数据结构(例如 JSON 对象),您必须将其序列化为上述格式之一,然后在另一端进行反序列化以重建数据结构。
用法示例:
如需了解双向消息交换的完整序列,事件会按以下顺序进行:
- 启动(应用):原生应用使用
addWebMessageListener注册监听器,并使用loadUrl()加载网页。 - 消息发送(网页):网页的 JavaScript 调用
myObject.postMessage(message)来启动通信。 - 消息接收和回复(应用):应用在监听器回调中接收消息,并使用提供的
replyProxy.postMessage()进行回复。 - 接收回复(网页):网页在
myObject.onmessage()回调函数中接收异步回复。
Kotlin
val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
// Handle the message from JS
replyProxy.postMessage("Acknowledged!")
}
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val allowedOrigins = setOf("https://www.example.com")
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}
Java
WebMessageListener myListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Handle the message from JS
replyProxy.postMessage("Acknowledged!");
};
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
Set<String> allowedOrigins = Set.of("https://www.example.com");
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener);
}
以下 JavaScript 代码展示了 addWebMessageListener 的客户端实现,可让网页内容通过 myObject 代理接收来自原生应用的消息并发送自己的消息。
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
使用 postWebMessage(替代方案)
Android 引入了此功能,以提供类似于 Web 的 window.postMessage 的基于消息传递的异步替代方案。
运作方式:该应用使用 WebViewCompat.postWebMessage 将载荷发送到网页的主框架。如需建立双向通信渠道,您可以创建 WebMessageChannel,并通过消息将其中一个端口传递给 Web 内容。
特征:
- 异步:与
addWebMessageListener类似,此方法使用异步消息传递,可确保网页在应用于后台处理数据时仍能响应用户互动。 - 了解来源:您可以指定
targetOrigin,以确保 WebView 仅向受信任的网站传送数据。
限制:
- 范围:此 API 将通信限制为主框架。不支持直接寻址或向 iframe 发送消息。
- URI 限制:您无法将此方法用于使用
data:URI、file:URI 或loadData()加载的内容,除非您将目标来源指定为“*”。这样,任何网页都可以接收消息。 - 身份风险:网页内容无法明确验证发件人的身份。网页收到的消息可能源自您的原生应用或其他 iframe。
如果您的 Android 版本较旧,不支持 addWebMessageListener,但您需要一个简单的异步通道来传输基于字符串的数据,请使用此方法。
使用 addJavascriptInterface(旧版)
最旧的方法是直接将原生对象实例注入到 WebView 中。
工作原理:您需要定义一个 Kotlin 或 Java 类,使用 @JavascriptInterface 注释允许的方法,然后使用 addJavascriptInterface(Object, String) 将该类的实例添加到 WebView。
特征:
- 同步:JavaScript 执行环境会一直处于阻塞状态,直到 Android 代码中的方法返回。
- 线程安全:系统会在后台线程上调用方法,因此需要在 Kotlin 或 Java 端进行仔细的同步。
- 安全风险:默认情况下,WebView 中的每个框架(包括 iframe)都可以使用
addJavascriptInterface。它缺少基于来源的访问权限控制。由于 WebView 的异步行为,无法安全地确定调用您的接口的框架的网址。您不得依赖WebView.getUrl()等方法进行安全验证,因为这些方法无法保证准确性,并且不会指明是哪个特定框架发出了请求。
机制摘要
下表简要比较了三种主要的原生桥接实现机制:
| 方法 | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| 实现 | 异步(主线程上的监听器) | 异步 | 同步 |
| 安全 | 最高(基于许可名单) | 高(了解来源) | 低(不检查来源) |
| 复杂性 | 中 | 中 | 简单 |
| 方向 | 双向 | 双向 | 从网站到应用 |
| 最低 WebView 版本 | 版本 82(和 Jetpack Webkit 1.3.0) | 版本 45(和 Jetpack Webkit 1.1.0) | 所有版本 |
| 推荐 | 是 | 否 | 否 |
处理大型数据传输
在传输大型载荷(例如数兆字节的字符串或二进制文件)时,您必须谨慎管理内存,以避免在 32 位设备上出现应用无响应 (ANR) 错误或崩溃。本部分将讨论在宿主应用和 Web 内容之间传输大量数据时涉及的各种技术和限制。
使用字节数组传输二进制数据
借助 WebMessageCompat 类,您可以直接发送 byte[] 数组,而无需将二进制数据序列化为 Base64 字符串。由于 Base64 会使数据大小增加大约 33% 的开销,因此这种方法在内存使用方面效率更高,速度也更快。
- 二进制优势:在原生应用和 Web 内容之间传输图片文件或音频等二进制数据。
- 限制:即使使用字节数组,系统也会在应用与 WebView 用于呈现网页内容的隔离进程之间的进程间通信 (IPC) 边界复制数据。不过,对于非常大的文件,这种方法仍会消耗大量内存。
以下代码示例演示了如何在原生应用端设置 addWebMessageListener 以接收标记为 WebMessageCompat.TYPE_ARRAY_BUFFER 的消息,并通过检查 WebViewFeature.MESSAGE_ARRAY_BUFFER 可选地回复二进制数据。
Kotlin
fun setupWebView(webView: WebView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val listener = WebViewCompat.WebMessageListener { view, message, sourceOrigin, isMainFrame, replyProxy ->
// Check if the received message is an ArrayBuffer
if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
val binaryData: ByteArray = message.arrayBuffer
// Process your binary data (image, audio, etc.)
println("Received bytes: ${binaryData.size}")
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
val replyBytes = byteArrayOf(0x01, 0x02, 0x03)
replyProxy.postMessage(replyBytes)
}
}
}
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
setOf("https://example.com"), // Security: restrict origins
listener
)
}
}
Java
public void setupWebView(WebView webView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.WebMessageListener listener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Check if the received message is an ArrayBuffer
if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
byte[] binaryData = message.getArrayBuffer();
// Process your binary data (image, audio, etc.)
System.out.println("Received bytes: " + binaryData.length);
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
byte[] replyBytes = new byte[]{0x01, 0x02, 0x03};
replyProxy.postMessage(replyBytes);
}
}
};
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
Set.of("https://example.com"), // Security: restrict origins
listener
);
}
}
以下 JavaScript 代码演示了 addWebMessageListener 的客户端实现,使 Web 内容能够使用上一个示例中注入的 window.myBridge 代理与原生应用之间发送和接收二进制数据 (ArrayBuffer)。
// Function to send an image or binary buffer to the app
async function sendBinaryToApp() {
const response = await fetch('image.jpg');
const buffer = await response.arrayBuffer();
// Check if the injected bridge object exists
if (window.myBridge) {
// You can send the ArrayBuffer directly
window.myBridge.postMessage(buffer);
}
}
// Receiving binary data from the app
if (window.myBridge) {
window.myBridge.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
console.log('Received binary data from App, length:', event.data.byteLength);
// Process the binary data (for example, as a Uint8Array)
const bytes = new Uint8Array(event.data);
console.log('First byte:', bytes[0]);
}
};
}
高效的大规模数据加载
对于非常大的文件(超过 10 MB),请使用 shouldInterceptRequest 方法来流式传输数据:
- 网页向自定义占位网址发起
fetch()调用。例如,https://app.local/large-file。 - Android 应用会在
WebViewClient.shouldInterceptRequest 中拦截此请求。 - 应用以
InputStream形式返回数据。
这样一来,就可以分块流式传输数据,而不是一次性将整个载荷加载到内存中。
以下 JavaScript 函数演示了客户端代码,该代码使用对自定义占位网址的标准 fetch() 调用,从原生应用高效加载大型二进制文件。
async function fetchBinaryFromApp() {
try {
// This URL doesn't need to exist on the internet
const response = await fetch('https://app.local/data/large-file.bin');
if (!response.ok) throw new Error('Network response was not okay');
// For raw binary data:
const arrayBuffer = await response.arrayBuffer();
console.log('Received binary data, size:', arrayBuffer.byteLength);
// Process buffer (for example, new Uint8Array(arrayBuffer))
/*
// OR for an image:
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
document.getElementById('myImage').src = imageUrl;
*/
} catch (error) {
console.error('Fetch error:', error);
}
}
以下代码示例演示了原生应用端如何使用 Kotlin 和 Java 中的 WebViewClient.shouldInterceptRequest 方法,通过拦截 Web 内容请求的自定义占位网址来流式传输大型二进制文件。
Kotlin
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request?.url ?: return null
// Check if this is our custom placeholder URL
if (url.host == "app.local" && url.path == "/data/large-file.bin") {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
val inputStream: InputStream = context.assets.open("my_data.pb")
// 2. Define Response Headers (Crucial for CORS/Fetch)
val headers = mutableMapOf<String, String>()
headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin
// 3. Return the response
return WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
)
} catch (e: Exception) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request)
}
}
Java
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String urlPath = request.getUrl().getPath();
String host = request.getUrl().getHost();
// Check if this is our custom placeholder URL
if ("app.local".equals(host) && "/data/large-file.bin".equals(urlPath)) {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
InputStream inputStream = getContext().getAssets().open("my_data.pb");
// 2. Define Response Headers (Crucial for CORS/Fetch)
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*"); // Allow fetch from any origin
// 3. Return the response
return new WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
);
} catch (Exception e) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request);
}
});
遵循安全建议
为保护您的应用和用户数据,在实现桥接时请遵循以下准则:
强制执行 HTTPS:为确保恶意第三方内容无法调用应用的原生逻辑,请仅允许与安全来源进行通信。
依赖来源规则:处理信任问题的最佳方法是严格定义
allowedOriginRules并检查消息回调中提供的sourceOrigin。除非绝对必要,否则请避免使用与所有来源匹配的完整通配符 (*) 作为唯一的来源规则。使用通配符指代子网域(例如*.example.com)对于匹配多个子网域(例如foo.example.com、bar.example.com)仍然有效且安全。注意:虽然来源规则可防范恶意第三方网站和隐藏的 iframe,但无法防范您自己的可信网域中的跨站脚本攻击 (XSS) 漏洞。例如,如果您的网页显示用户生成的内容,并且容易受到存储型 XSS 攻击,攻击者就可以执行充当您可信来源的脚本。考虑在执行敏感的原生平台操作之前,对消息载荷应用验证。
尽量缩小公开范围:仅公开网页所需的特定方法或数据。
在运行时检查功能:最新的桥接 API(包括
addWebMessageListener)是 Jetpack Webkit 库的一部分。因此,在致电之前,请务必先使用WebViewFeature.isFeatureSupported()检查是否支持。