Über die JavaScript-Bridge auf native APIs zugreifen

Auf dieser Seite werden die verschiedenen Methoden und Best Practices zum Einrichten einer nativen Bridge, auch JavaScript-Bridge genannt, beschrieben, um die Kommunikation zwischen Webinhalten in einer WebView und einer Host-Android-App zu ermöglichen.

So können Webentwickler mit JavaScript auf native Plattformfunktionen wie die Kamera, das Dateisystem oder erweiterte Hardwaresensoren zugreifen, die von Standard-Web-APIs normalerweise nicht bereitgestellt werden.

Anwendungsfälle

Eine JavaScript-Bridge-Implementierung ermöglicht verschiedene Integrationsszenarien, in denen Webinhalte einen tieferen Zugriff auf das Android-Betriebssystem benötigen. Hier einige Beispiele:

  • Plattformintegration: Auslösen nativer Android-UI-Komponenten (z. B. biometrische Eingabeaufforderungen, BottomSheetDialog) von einer Webseite aus.
  • Leistung: Auslagern rechenintensiver Aufgaben an nativen Java- oder Kotlin-Code.
  • Datenpersistenz: Zugriff auf lokale verschlüsselte Datenbanken oder freigegebene Einstellungen.
  • Große Datenübertragungen: Übergeben von Mediendateien oder komplexen Datenstrukturen zwischen der App und dem Web-Renderer.

Kommunikationsmechanismen

Android bietet drei Hauptgenerationen von APIs zum Einrichten einer nativen Bridge. Sie sind alle noch verfügbar, unterscheiden sich aber erheblich in Bezug auf Sicherheit, Nutzerfreundlichkeit und Leistung.

addWebMessageListener verwenden (empfohlen)

addWebMessageListener ist der modernste und empfohlene Ansatz für die Kommunikation zwischen Webinhalten und systemeigenem App-Code. Er kombiniert die Nutzerfreundlichkeit der JavaScript-Schnittstelle mit der Sicherheit des Nachrichtensystems.

Funktionsweise: Die App fügt einen Listener mit einem bestimmten Namen und einer Reihe zulässiger Ursprungsregeln hinzu. Die WebView sorgt dann dafür, dass das JavaScript-Objekt ab dem Beginn des Ladens der Seite im globalen Bereich (window.objectName) vorhanden ist.

Initialisierung: Damit die WebView das JavaScript-Objekt einfügt, bevor ein Skript ausgeführt wird, müssen Sie addWebMessageListener aufrufen, bevor Sie loadUrl() aufrufen.

Wichtige Funktionen:

  • Sicherheit und Vertrauen: Im Gegensatz zu älteren APIs erfordert diese Methode bei der Initialisierung ein Set<String> von allowedOriginRules. Dies ist der primäre Mechanismus zum Aufbau von Vertrauen.

    Wenn Sie einen vertrauenswürdigen Ursprung wie https://example.com angeben, garantiert die WebView, dass die eingefügten JavaScript-Objekte nur für Webseiten verfügbar sind, die von diesem Ursprung geladen wurden.

    Der native Listener-Callback erhält mit jeder Nachricht einen sourceOrigin-Parameter. Damit können Sie den genauen Ursprung des Absenders überprüfen, wenn Ihre Bridge mehrere zulässige Ursprünge unterstützt.

    Da die WebView diese Ursprungsprüfungen auf Plattformebene streng durchsetzt, können Sie sich in der Regel darauf verlassen, dass Nachrichten, die von einem vertrauenswürdigen sourceOrigin stammen, wahr sind. In den meisten Standardimplementierungen ist daher keine strenge Nutzlastvalidierung erforderlich.

    • Die WebView vergleicht Regeln mit dem Schema (HTTP/HTTPS), dem Host und dem Port.
    • Die WebView ignoriert Pfade. Beispielsweise lässt https://example.com die Ursprünge https://example.com/login und https://example.com/home zu.
    • Die WebView beschränkt Platzhalter streng auf den Anfang des Hosts für Subdomains. Beispielsweise stimmt https://*.example.com mit https://foo.example.com überein, aber nicht mit https://example.com. Wenn Sie sowohl https://example.com als auch die Subdomains abgleichen müssen, müssen Sie jede Ursprungsregel separat zur Zulassungsliste hinzufügen (z. B. "https://example.com", "https://*.example.com"). Platzhalter können nicht für das Schema oder in der Mitte einer Domain verwendet werden.

    Dadurch wird die Bridge auf bestätigte Domains beschränkt und verhindert, dass nicht autorisierte Inhalte von Dritten oder eingefügte iFrames nativen Code ausführen.

  • Unterstützung für mehrere Frames: Funktioniert in allen Frames, die den Ursprungs regeln entsprechen.

  • Threading: Der Listener-Callback wird im Hauptthread (UI) der Anwendung ausgeführt. Wenn Ihre Bridge komplexe Datenverarbeitung, JSON-Parsing oder Datenbankabfragen verarbeiten muss, müssen Sie diese Arbeit an einen Hintergrundthread auslagern, um zu verhindern, dass die Benutzeroberfläche der Anwendung mit einem ANR-Fehler („App antwortet nicht“) einfriert.

  • Bidirektional: Wenn die Webseite eine Nachricht sendet, erhält die App einen JavaScriptReplyProxy, mit dem sie Nachrichten an diesen bestimmten Frame zurücksenden kann. Sie können dieses replyProxy-Objekt beibehalten und jederzeit verwenden, um eine beliebige Anzahl von Nachrichten an die Seite zu senden, nicht nur um auf jede einzelne Nachricht zu antworten, die die Seite sendet. Wenn der ursprüngliche Frame zu einer anderen Seite navigiert oder zerstört wird, werden Nachrichten, die mit postMessage() über den Proxy gesendet werden, stillschweigend ignoriert.

  • Initiierung auf App-Seite: Obwohl die Webseite immer den Kommunikationskanal mit der App initiieren muss, kann die systemeigene App die Webseite einseitig auffordern, diesen Prozess zu starten. Die systemeigene App kann mit der Webseite mit addDocumentStartJavaScript() (um JavaScript vor dem Laden der Seite auszuwerten) oder evaluateJavaScript() (um JavaScript nach dem Laden der Seite auszuwerten) kommunizieren.

Einschränkung: Diese API sendet Daten entweder als Strings oder als byte[] Arrays. Bei komplizierteren Datenstrukturen wie JSON-Objekten müssen Sie diese in eines dieser Formate serialisieren und dann auf der anderen Seite deserialisieren, um die Datenstruktur wiederherzustellen.

Verwendungsbeispiel:

Um die vollständige Sequenz eines bidirektionalen Nachrichtenaustauschs zu verstehen, werden die Ereignisse in dieser Reihenfolge ausgeführt:

  1. Initiierung (App): Die systemeigene App registriert den Listener mit addWebMessageListener und lädt die Webseite mit loadUrl().
  2. Nachricht senden (Web): Das JavaScript der Webseite ruft myObject.postMessage(message) auf, um die Kommunikation zu initiieren.
  3. Nachricht empfangen und antworten (App): Die App empfängt die Nachricht im Listener-Callback und antwortet mit dem bereitgestellten replyProxy.postMessage().
  4. Antwort empfangen (Web): Die Webseite empfängt die asynchrone Antwort in der myObject.onmessage() Callback-Funktion.

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);
}

Das folgende JavaScript zeigt die clientseitige Implementierung von addWebMessageListener, mit der die Webinhalte Nachrichten von der systemeigenen App empfangen und eigene Nachrichten über den myObject-Proxy senden können.

myObject.onmessage = function(event) {
    console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");

postWebMessage verwenden (Alternative)

Android hat diese Methode eingeführt, um eine asynchrone, nachrichtenbasierte Alternative zu window.postMessage im Web bereitzustellen.

Funktionsweise: Die App verwendet WebViewCompat.postWebMessage, um eine Nutzlast an den Hauptframe der Webseite zu senden. Um einen bidirektionalen Kommunikations kanal einzurichten, können Sie einen WebMessageChannel erstellen und einen seiner Ports mit der Nachricht an die Webinhalte übergeben.

Eigenschaften:

  • Asynchron: Wie addWebMessageListener verwendet diese Methode asynchrone Nachrichten, wodurch sichergestellt wird, dass die Webseite auf Nutzerinteraktionen reagiert, während die App Daten im Hintergrund verarbeitet.
  • Ursprungsabhängig: Sie können einen targetOrigin angeben, um sicherzustellen, dass die WebView Daten nur an eine vertrauenswürdige Website liefert.

Einschränkungen:

  • Bereich: Diese API beschränkt die Kommunikation auf den Hauptframe. Sie unterstützt nicht das direkte Adressieren oder Senden von Nachrichten an iFrames.
  • URI-Einschränkungen: Sie können diese Methode nicht für Inhalte verwenden, die mit data: URIs, file: URIs oder loadData() geladen wurden, es sei denn, Sie geben „*“ als den Zielursprung an. Dadurch kann jede Seite die Nachricht empfangen.
  • Identitätsrisiko: Es gibt keine klare Möglichkeit für die Webinhalte, die Identität des Absenders zu überprüfen. Eine Nachricht, die die Webseite empfängt, kann von Ihrer systemeigenen App oder einem anderen iFrame stammen.

Verwenden Sie diese Methode, wenn Sie einen einfachen, asynchronen Kanal für stringbasierte Daten in älteren Android-Versionen benötigen, die addWebMessageListener nicht unterstützen.

addJavascriptInterface verwenden (Legacy)

Bei der ältesten Methode wird eine native Objektinstanz direkt in die WebView eingefügt.

Funktionsweise: Sie definieren eine Kotlin- oder Java-Klasse, versehen die zulässigen Methoden mit @JavascriptInterface und fügen der WebView mit addJavascriptInterface(Object, String) eine Instanz der Klasse hinzu.

Eigenschaften:

  • Synchron: Die JavaScript-Ausführungsumgebung wird blockiert, bis die Methode in Ihrem Android-Code zurückgegeben wird.
  • Threadsicherheit: Das System ruft Methoden in einem Hintergrundthread auf, daher ist eine sorgfältige Synchronisierung auf Kotlin- oder Java-Seite erforderlich.
  • Sicherheitsrisiko: Standardmäßig ist addJavascriptInterface für jeden Frame in der WebView verfügbar, einschließlich iFrames. Es gibt keine ursprungsbasierte Zugriffssteuerung. Aufgrund des asynchronen Verhaltens der WebView ist es nicht möglich, die URL des Frames, der Ihre Schnittstelle aufruft, sicher zu bestimmen. Sie dürfen sich nicht auf Methoden wie WebView.getUrl() zur Sicherheits überprüfung verlassen, da sie nicht garantiert genau sind und nicht angeben welcher Frame die Anfrage gestellt hat.

Zusammenfassung der Mechanismen

Die folgende Tabelle enthält einen kurzen Vergleich der drei primären Mechanismen zur Implementierung einer nativen Bridge:

Methode addWebMessageListener postWebMessage addJavascriptInterface
Implementierung Asynchron (Listener im Hauptthread) Asynchron Synchron
Sicherheit Höchste (basierend auf Zulassungsliste) Hoch (ursprungsabhängig) Niedrig (keine Ursprungsprüfungen)
Komplexität Moderat Moderat Einfach
Richtung Bidirektional Bidirektional Web zu App
Mindestversion der WebView Version 82 (und Jetpack Webkit 1.3.0) Version 45 (und Jetpack Webkit 1.1.0) Alle Versionen
Empfohlen Ja Nein Nein

Große Datenübertragungen verarbeiten

Sie müssen den Arbeitsspeicher sorgfältig verwalten, wenn Sie große Nutzlasten wie Strings mit mehreren Megabyte oder Binärdateien übertragen, um ANR-Fehler („App antwortet nicht“) oder Abstürze auf 32-Bit-Geräten zu vermeiden. In diesem Abschnitt werden die verschiedenen Techniken und Einschränkungen im Zusammenhang mit der Übertragung großer Datenmengen zwischen der Hostanwendung und Webinhalten beschrieben.

Binärdaten mit Bytearrays übertragen

Mit der WebMessageCompat-Klasse können Sie byte[]-Arrays direkt senden, anstatt Binärdaten in Base64-Strings zu serialisieren. Da Base64 die Datengröße um etwa 33% erhöht, ist dies deutlich speichereffizienter und schneller.

  • Vorteil von Binärdaten: Übertragen Sie Binärdaten wie Bild- oder Audiodateien zwischen Ihrer systemeigenen App und Webinhalten.
  • Einschränkung: Auch bei Bytearrays kopiert das System Daten über die IPC-Grenze (Inter-Process Communication) zwischen der App und dem isolierten Prozess, den die WebView zum Rendern der Webinhalte verwendet. Bei sehr großen Dateien wird dadurch immer noch viel Arbeitsspeicher belegt.

Die folgenden Codebeispiele zeigen, wie Sie addWebMessageListener auf der Seite der systemeigenen App einrichten, um Nachrichten zu empfangen, die mit WebMessageCompat.TYPE_ARRAY_BUFFER gekennzeichnet sind, und optional mit Binärdaten zu antworten, indem Sie nach WebViewFeature.MESSAGE_ARRAY_BUFFER suchen.

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
      );
  }
}

Der folgende JavaScript-Code zeigt die clientseitige Implementierung von addWebMessageListener, mit der die Webinhalte Binärdaten (ArrayBuffer) über den im vorherigen Beispiel eingefügten window.myBridge-Proxy an die systemeigene App senden und von ihr empfangen können.

// 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]);
        }
    };
}

Effizientes Laden großer Datenmengen

Verwenden Sie für sehr große Dateien (> 10 MB) die shouldInterceptRequest Methode, um Daten zu streamen:

  1. Die Webseite initiiert einen fetch()-Aufruf an eine benutzerdefinierte Platzhalter-URL. Beispiel: https://app.local/large-file.
  2. Die Android-App fängt diese Anfrage in WebViewClient.shouldInterceptRequest ab.
  3. Die App gibt die Daten als InputStream zurück.

So können Daten in Blöcken gestreamt werden, anstatt die gesamte Nutzlast auf einmal in den Arbeitsspeicher zu laden.

Die folgende JavaScript-Funktion zeigt den clientseitigen Code zum effizienten Laden einer großen Binärdatei aus der nativen Anwendung mit einem Standard-fetch()-Aufruf an eine benutzerdefinierte Platzhalter-URL.

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);
    }
}

Die folgenden Codebeispiele zeigen die Seite der systemeigenen App, wobei die Methode WebViewClient.shouldInterceptRequest sowohl in Kotlin als auch in Java verwendet wird, um eine große Binärdatei zu streamen, indem eine benutzerdefinierte Platzhalter-URL abgefangen wird, die von den Webinhalten angefordert wurde.

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);
  }
});

Sicherheitsempfehlungen befolgen

Um Ihre Anwendung und Nutzerdaten zu schützen, sollten Sie beim Implementieren einer Bridge die folgenden Richtlinien beachten:

  • HTTPS erzwingen: Damit schädliche Inhalte von Dritten nicht die native Logik Ihrer Anwendung aufrufen können, lassen Sie nur die Kommunikation mit sicheren Ursprüngen zu.

  • Ursprungsregeln verwenden: Die beste Möglichkeit, mit Vertrauen umzugehen, besteht darin, Ihre allowedOriginRules genau zu definieren und den in der Nachrichten-Callback-Funktion angegebenen sourceOrigin zu überprüfen. Verwenden Sie den vollständigen Platzhalter (*), der mit allen Ursprüngen übereinstimmt, nur als Ursprungsregel, wenn dies unbedingt erforderlich ist. Die Verwendung von Platzhaltern für Subdomains (z. B. *.example.com) ist weiterhin gültig und sicher, um mehrere Subdomains abzugleichen (z. B. foo.example.com, bar.example.com).

    Hinweis: Ursprungsregeln schützen zwar vor schädlichen Websites von Drittanbietern und verborgenen iFrames, aber nicht vor XSS-Sicherheitslücken (Cross-Site-Scripting) in Ihrer eigenen vertrauenswürdigen Domain. Wenn Ihre Webseite beispielsweise von Nutzern erstellte Inhalte anzeigt und anfällig für gespeichertes XSS ist, könnte ein Angreifer ein Skript ausführen, das als Ihr vertrauenswürdiger Ursprung fungiert. Sie sollten die Nutzlasten der Nachrichten validieren, bevor Sie vertrauliche native Plattformvorgänge ausführen.

  • Angriffsfläche minimieren: Stellen Sie nur die spezifischen Methoden oder Daten bereit, die die Webseite benötigt.

  • Funktionen zur Laufzeit prüfen: Aktuelle Bridge-APIs, einschließlich addWebMessageListener, sind Teil der Jetpack Webkit-Bibliothek. Prüfen Sie daher immer mit WebViewFeature.isFeatureSupported(), ob sie unterstützt werden, bevor Sie sie aufrufen.