本页讨论了用于建立原生桥(也称为 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(例如调用 WebViewCompat.navigate 或 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 执行原生代码。
多框架支持:适用于与来源规则匹配的所有框架。
线程:监听器回调在应用的主 (UI) 线程上运行。如果您的桥接需要处理复杂的数据处理、JSON 解析或数据库查找,您必须将该工作分流到后台线程,以防止应用界面因“应用无响应”(ANR) 错误而冻结。
双向:当网页发送消息时,应用会收到一个
JavaScriptReplyProxy,该对象可用于将消息发送回该特定框架。您可以保留此replyProxy对象,并随时使用它向网页发送任意数量的消息,而不仅仅是回复网页发送的每条消息。如果原始框架离开或被销毁,则使用代理上的postMessage()发送的消息会被静默忽略。应用端启动:虽然网页必须始终启动与应用的通信渠道,但原生应用可以单方面提示网页开始此过程。原生应用可以使用
addDocumentStartJavaScript()(在网页加载之前评估 JavaScript)或evaluateJavaScript()(在网页加载之后评估 JavaScript)与网页通信。
限制:此 API 以字符串或 byte[] 数组的形式发送数据。对于更复杂的数据结构(例如 JSON 对象),您必须将其序列化为上述格式之一,然后在另一端进行反序列化以重建数据结构。
用法示例:
如需了解双向消息交换的完整序列,请按以下顺序查看事件:
- 启动(应用):原生应用使用
addWebMessageListener注册监听器,并启动网页导航(例如使用WebViewCompat.navigate或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()等方法进行安全验证,因为这些方法无法保证准确性,并且无法指明是哪个特定框架发出了请求。
数据类型转换和强制转换
使用 addJavascriptInterface 时,基于 Chromium 的 Java 桥接机制会在 JavaScript 运行时和 Android 应用代码之间转换数据类型。
以下强制转换规则适用于方法参数和返回值。
参数类型映射(JavaScript 到 Java)
当 JavaScript 将实参传递给带注释的 Java 或 Kotlin 方法时,桥接器会将 JavaScript 值强制转换为相应的 Java 形参类型:
| Java 参数类型 | JavaScript 实参值 | 强制转换行为 |
|---|---|---|
byte、short、int、long |
数字(整数) | 值会被强制转换为目标整数类型。根据标准数值转换规则,超出范围的值会回绕。 |
byte、short、int、long |
NaN |
强制转换为 0。 |
byte、short、int、long |
Infinity |
强制转换为 -1(针对 byte 和 short)或 Integer.MAX_VALUE 和 Long.MAX_VALUE(针对 int 和 long)。 |
float、double |
数字 | 强制转换为相应的 Java 浮点值。 |
float、double |
NaN/Infinity |
强制转换为 Float.NaN、Double.NaN、Float.POSITIVE_INFINITY 或 Double.POSITIVE_INFINITY。 |
char |
数字(整数) | 转换为相应的 Unicode 码位。 |
char |
非整数,NaN,Infinity |
强制转换为 \u0000。 |
boolean |
true/false |
强制转换为 Java true 或 false。 |
boolean |
数字、字符串、对象 | 强制转换为 false(包括非空字符串和非零数字)。 |
String |
字符串 | 字符串值会保留。 |
String |
数字、布尔值 | 格式为字符串表示形式(例如 "42"、"true"、"false")。 |
String |
null/undefined |
null 强制转换为 Java null;undefined 强制转换为字面量字符串 "undefined"。 |
String |
Object、ArrayBuffer、TypedArray | 强制转换为字面值字符串 "undefined"。 |
原始数组(例如 int[]、byte[]、boolean[])或 String[] |
数组 ([...]) |
转换为目标元素类型的一维 Java 数组。稀疏数组会使用默认值(0、false、null)填充未分配的索引。 |
原始数组(例如 int[]、byte[]) |
TypedArray(Int8Array、Uint8Array、Int32Array、Float64Array) |
元素会被强制转换为相应的 Java 基元数组。 |
多维数组(例如 int[][]) |
嵌套数组 ([[...]]) |
不支持。多维数组形参的计算结果为 null。 |
ArrayBuffer、DataView |
ArrayBuffer、DataView |
不支持以数组形式显示。ArrayBuffer 和 DataView 实例的求值结果为 null。 |
Object 或自定义类 |
JavaScript 对象 ({...}) |
不支持。任意 JavaScript 对象字面量在 Java 中求值为 null。 |
Object 或自定义类 |
注入的 Java 对象封装容器 | 支持(往返)。将底层 Java 实例传递给 Java 方法。如果 Java 类型与参数签名不匹配,则抛出 JavaScript 异常。 |
装箱类型(例如 Integer、Double、Boolean) |
数字、布尔值 | 不支持。封装的基元类型会被视为不透明的对象,并评估为 null。 |
| 任何原初类型 | null/undefined |
强制转换为默认值(0、0.0、\u0000、false)。 |
Object,String,数组 |
null |
强制转换为 Java null。 |
返回值类型映射(Java 到 JavaScript)
当带注释的 Java 或 Kotlin 方法返回值时,桥接器会将其转换为 JavaScript 类型:
| Java 返回值类型 | JavaScript 值 | JavaScript typeof |
|---|---|---|
boolean |
true/false |
"boolean" |
byte、short、int、long、float、double |
数字 | "number" |
char |
数字(Unicode 码位) | "number" |
String(非 null) |
字符串值 | "string" |
String (null) |
undefined |
"undefined" |
void |
undefined |
"undefined" |
Java 数组(例如 int[]、String[]) |
undefined |
"undefined"。不支持数组返回值。系统不会执行 Java 方法,并且会返回 undefined,而不会引发异常。 |
| Java 对象 / 自定义类型(非 null) | 对象封装容器 | "object". 在 Java 实例周围创建 JavaScript 封装容器。JavaScript 代码可以调用此对象上使用 @JavascriptInterface 注释的任何公共方法。 |
Java 对象 / 自定义类型 (null) |
null |
"object" |
装箱的原初类型(例如 Integer、Double) |
对象封装容器 | "object"。作为不含可访问 @JavascriptInterface 方法的不透明 Java 对象封装容器返回,导致该值在 JavaScript 中无法使用。 |
方法和成员可访问性
JavaScript 桥接会强制执行严格的成员访问和可见性规则,以防止意外的代码执行:
- 字段未公开:Java 字段(包括
public和public final字段)无法从 JavaScript 访问,并且会评估为undefined。 - 注释要求:只有明确使用
@JavascriptInterface注释的方法才会向 JavaScript 公开。 - 公开范围限制:方法必须为
public。即使private和protected方法带有@JavascriptInterface注释,也绝不会向 JavaScript 公开。 - 静态方法:带有
@JavascriptInterface注释的静态方法可从 JavaScript 调用。 - 继承和替换:当子类替换方法时,不会继承
@JavascriptInterface注释。如果子类替换了父类中的带注释的方法,则子类必须在被替换的方法上明确包含@JavascriptInterface注释,以便将其公开给 JavaScript。如果从超类继承的未被替换的公共方法在超类中进行了注释,则仍可访问。 - 反射保护:标准 Java 反射方法(例如
getClass())会被屏蔽并抛出 JavaScript 异常,以防止出现远程代码执行漏洞。 - 方法重载:支持重载的 Java 方法。该桥接仅根据传递的实参数量来解析方法调用,而不考虑实参类型。使用无效的实参数量调用重载方法会引发 JavaScript 异常。如果两个重载具有相同的实参数量,系统会任意选择一个。
机制摘要
下表简要比较了三种主要的原生桥接实现机制:
| 方法 | 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()检查是否支持。