使用 JavaScript 桥接访问原生 API

本页面将讨论建立原生桥(也称为 JavaScript 桥)的各种方法和最佳实践,以促进 WebView 中的 Web 内容与宿主 Android 应用之间的通信。

这让 Web 开发者能够使用 JavaScript 访问原生平台功能(例如摄像头、文件系统或高级硬件传感器),而标准 Web API 通常不提供这些功能。

使用场景

JavaScript 桥实现支持各种集成场景,在这些场景中,Web 内容需要更深入地访问 Android 操作系统。下面列出了一些示例:

  • 平台集成:从网页触发原生 Android 界面组件(例如生物识别提示、BottomSheetDialog)。
  • 性能:将繁重的计算任务卸载到原生 Java 或 Kotlin 代码。
  • 数据持久性:访问本地加密数据库或共享 偏好设置。
  • 大型数据传输:在应用和 Web 渲染器之间传递媒体文件或复杂的数据结构 。

通信机制

Android 提供了三代主要的 API 来建立原生桥。 虽然这些 API 仍然可用,但在安全性、易用性和性能方面存在显著差异。

使用 addWebMessageListener(推荐)

addWebMessageListener 是 Web 内容与原生应用代码之间通信的最新方法,也是我们推荐的方法。它兼具 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/loginhttps://example.com/home
    • WebView 严格限制通配符仅用于子网域的主机开头。例如,https://*.example.comhttps://foo.example.com 匹配,但与 https://example.com 不匹配。如果您需要同时匹配 https://example.com 及其子网域,则必须将每个来源规则单独添加到许可名单中(例如 "https://example.com", "https://*.example.com")。您不能对架构使用通配符,也不能在网域中间使用通配符。

    这会将桥限制为经过验证的网域,从而防止未经授权的第三方内容或注入的 iframe 执行原生代码。

  • 多框架支持:适用于与来源 规则匹配的所有框架。

  • 线程:监听器回调在应用的主(界面) 线程上运行。如果您的桥需要处理复杂的数据处理、JSON 解析或数据库查询,您必须将该工作分流到后台线程,以防止应用界面因“应用无响应”(ANR) 错误而冻结。

  • 双向:当网页发送消息时,应用会收到一个 JavaScriptReplyProxy,它可以使用该代理将消息发送回该 特定框架。您可以保留此 replyProxy 对象,并随时使用它向网页发送任意数量的消息,而不仅仅是回复网页发送的每条单独消息。如果原始框架导航离开或被销毁,则使用代理上的 postMessage() 发送的消息会被静默忽略。

  • 应用端启动:虽然网页必须始终启动与应用的 通信渠道,但原生应用可以单方面提示 网页开始此过程。原生应用可以使用 网页与 addDocumentStartJavaScript()(在网页加载之前评估 JavaScript )或 evaluateJavaScript()(在网页加载之后评估 JavaScript)通信。

限制:此 API 以字符串或 byte[] 数组的形式发送数据。对于更复杂的数据结构(例如 JSON 对象),您必须将其序列化为其中一种格式,然后在另一端进行反序列化以重建数据结构。

用法示例

如需了解双向消息交换的完整序列,事件按以下顺序进行:

  1. 启动(应用):原生应用使用 addWebMessageListener注册监听器,并使用loadUrl()加载网页。
  2. 消息发送(网页):网页的 JavaScript 调用 myObject.postMessage(message) 以启动通信。
  3. 消息接收和回复(应用):应用在 监听器回调中接收消息,并使用提供的 replyProxy.postMessage() 进行回复。
  4. 回复接收(网页):网页在 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 的客户端实现,允许 Web 内容通过 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() 加载的内容。这样做可让任何网页接收消息。
  • 身份风险:Web 内容无法清楚地验证 发送者的身份。网页收到的消息可能来自您的原生应用或其他 iframe。

当您需要在不支持 addWebMessageListener 的较低 Android 版本中使用简单的异步渠道来处理基于字符串的数据时,请使用此方法。

使用 addJavascriptInterface(旧版)

最旧的方法涉及将原生对象实例直接注入 WebView。

工作原理:您需要定义 Kotlin 或 Java 类,使用 @JavascriptInterface 注解允许的 方法,并使用 addJavascriptInterface(Object, String) 将该类的实例添加到 WebView。

特点

  • 同步:JavaScript 执行环境会一直处于阻塞状态,直到 Android 代码中的方法返回为止。
  • 线程安全:系统会在后台线程上调用方法, 因此需要在 Kotlin 或 Java 端进行仔细同步。
  • 安全风险:默认情况下,addJavascriptInterface 可供 WebView 中的每个框架(包括 iframe)使用。它缺少基于来源的访问权限控制。由于 WebView 的异步行为,无法安全地确定调用您的界面的框架的网址。您不得依赖 WebView.getUrl() 等方法进行安全验证,因为这些方法无法保证准确性,并且不会指明哪个特定框架发出了请求。

机制摘要

下表简要比较了三种主要的原生桥实现机制:

方法 addWebMessageListener postWebMessage addJavascriptInterface
实现 异步(主线程上的监听器) 异步 同步
安全 最高(基于许可名单) 高(来源感知) 低(无来源检查)
复杂性 简单
方向 双向 双向 Web 到应用
最低 WebView 版本 版本 82(和 Jetpack Webkit 1.3.0) 版本 45(和 Jetpack Webkit 1.1.0) 所有版本
推荐

处理大型数据传输

在传输大型载荷(例如多兆字节的字符串或二进制文件)时,您必须仔细管理内存,以避免在 32 位设备上出现“应用无响应”(ANR) 错误或崩溃。本部分将讨论与在宿主应用和 Web 内容之间传输大量数据相关的各种技术和限制。

使用字节数组传输二进制数据

借助 WebMessageCompat 类,您可以直接发送 byte[] 数组 ,而无需将二进制数据序列化为 Base64 字符串。由于 Base64 会给数据大小增加大约 33% 的开销,因此这种方法在内存使用方面效率更高,速度也更快。

  • 二进制优势:在 原生应用和 Web 内容之间传输二进制数据(例如图片文件或音频)。
  • 限制:即使使用字节数组,系统也会在应用和 WebView 用于渲染 Web 内容的隔离进程之间的 进程间通信 (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 方法来 流式传输数据:

  1. 网页会向自定义占位符网址发起 fetch() 调用。例如,https://app.local/large-file
  2. Android 应用会在 WebViewClient.shouldInterceptRequest 中拦截此请求。
  3. 应用会将数据作为 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.combar.example.com)仍然有效且安全。

    注意:虽然来源规则可以防范恶意第三方网站 和隐藏的 iframe,但无法防范您自己的受信任网域中的跨站脚本攻击 (XSS) 漏洞。例如,如果您的网页显示用户生成的内容并且容易受到存储型 XSS 攻击,攻击者可能会执行充当受信任来源的脚本。请考虑在执行敏感的原生平台操作之前对消息载荷应用验证。

  • 最大限度地减少攻击面:仅公开 网页所需的特定方法或数据。

  • 在运行时检查功能:最新的桥 API(包括 addWebMessageListener)是 Jetpack Webkit 库的一部分。因此,在调用这些 API 之前,请务必 使用 WebViewFeature.isFeatureSupported() 检查是否支持它们。