Ü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 einem WebView und einer Host-Android-Anwendung zu ermöglichen.

So können Webentwickler mit JavaScript auf native Plattformfunktionen wie Kamera, 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 Aufforderungen, BottomSheetDialog) von einer Webseite aus.
  • Leistung: Auslagern rechenintensiver Aufgaben an nativen Java- oder Kotlin-Code.
  • Datenpersistenz: Zugriff auf lokale verschlüsselte Datenbanken oder gemeinsame Einstellungen.
  • Übertragung großer Datenmengen: Übertragung von Mediendateien oder komplexen Datenstrukturen zwischen der App und dem Web-Renderer.

Kommunikationsmechanismen

Android bietet drei Hauptgenerationen von APIs, um eine native Brücke zu schaffen. Sie sind zwar 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 Web-Inhalten und nativem App-Code. Es kombiniert die Benutzerfreundlichkeit der JavaScript-Schnittstelle mit der Sicherheit des Messaging-Systems.

So funktioniert es: Die App fügt einen Listener mit einem bestimmten Namen und einer Reihe von Regeln für zulässige Herkünfte hinzu. Die WebView sorgt dann dafür, dass das JavaScript-Objekt ab dem Moment, in dem die Seite geladen wird, im globalen Bereich (window.objectName) vorhanden ist.

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

Wichtige Funktionen:

  • Sicherheit und Vertrauen: Im Gegensatz zu Legacy-APIs ist für diese Methode bei der Initialisierung ein Set<String> von allowedOriginRules erforderlich. Dies ist der primäre Mechanismus zum Aufbau von Vertrauen.

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

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

    Da diese Ursprungsprüfungen auf Plattformebene streng durch WebView erzwungen werden, kann sich Ihre App in der Regel darauf verlassen, dass Nachrichten, die von einem vertrauenswürdigen sourceOrigin empfangen werden, wahrheitsgemäß sind. In den meisten Standardimplementierungen ist daher keine strenge Nutzlastvalidierung erforderlich.

    • WebView vergleicht Regeln mit dem Schema (HTTP/HTTPS), dem Host und dem Port.
    • WebView ignoriert Pfade. Beispiel: https://example.com erlaubt https://example.com/login und https://example.com/home.
    • In WebView sind Platzhalter für Subdomains streng auf den Anfang des Hosts beschränkt. Beispielsweise führt https://*.example.com zu Übereinstimmungen mit https://foo.example.com, aber nicht mit https://example.com. Wenn Sie sowohl https://example.com als auch die zugehörigen Subdomains abgleichen möchten, müssen Sie jede Ursprungsregel separat zur Zulassungsliste hinzufügen (z. B. "https://example.com", "https://*.example.com"). Sie können keine Platzhalter für das Schema oder in der Mitte einer Domain verwenden.

    Dadurch wird die Bridge auf verifizierte Domains beschränkt und verhindert, dass nicht autorisierte Drittanbieterinhalte oder eingefügte iFrames nativen Code ausführen.

  • Unterstützung mehrerer Frames: Funktioniert in allen Frames, die den Ursprungsregeln entsprechen.

  • Threading: Der Listener-Callback wird im Hauptthread (UI) der Anwendung ausgeführt. Wenn Ihre Bridge komplexe Datenverarbeitung, JSON-Parsing oder Datenbankabfragen ausführen muss, müssen Sie diese Aufgaben 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, empfängt die App ein 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 navigiert oder zerstört wird, werden Nachrichten, die über postMessage() an den Proxy gesendet werden, stillschweigend ignoriert.

  • Initiierung auf App-Seite: Der Kommunikationskanal mit der App muss zwar immer von der Webseite initiiert werden, die native App kann die Webseite jedoch einseitig auffordern, diesen Prozess zu starten. Die native App kann über addDocumentStartJavaScript() (zum Auswerten von JavaScript vor dem Laden der Seite) oder evaluateJavaScript() (zum Auswerten von JavaScript nach dem Laden der Seite) mit der Webseite kommunizieren.

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

Verwendungsbeispiel:

Die Ereignisse eines bidirektionalen Nachrichtenaustauschs laufen in dieser Reihenfolge ab:

  1. Initiierung (App): Die native App registriert den Listener mit addWebMessageListener und lädt die Webseite mit loadUrl().
  2. Nachricht senden (Web): Die JavaScript-Aufrufe der Webseite myObject.postMessage(message) initiieren die Kommunikation.
  3. Nachricht empfangen und beantworten (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 Callback-Funktion 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);
}

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

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

postWebMessage (Alternative) verwenden

Android hat diese Funktion eingeführt, um eine asynchrone, auf Messaging basierende Alternative ähnlich der window.postMessage-Funktion im Web bereitzustellen.

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

Merkmale:

  • Asynchron: Wie bei addWebMessageListener wird bei dieser Methode asynchrones Messaging verwendet. Dadurch bleibt die Webseite für Nutzerinteraktionen reaktionsfähig, während die App Daten im Hintergrund verarbeitet.
  • Ursprungsbezogen: Sie können ein targetOrigin angeben, damit die WebView Daten nur an eine vertrauenswürdige Website sendet.

Einschränkungen:

  • Bereich: Diese API beschränkt die Kommunikation auf den Hauptframe. Es unterstützt nicht das direkte Ansprechen oder Senden von Nachrichten an iFrames.
  • URI-Einschränkungen: Diese Methode kann nicht für Inhalte verwendet werden, die über data:-URIs, file:-URIs oder loadData() geladen werden, es sei denn, Sie geben „*“ als Zielursprung an. So kann die Nachricht auf jeder Seite empfangen werden.
  • Identitätsrisiko: Es gibt keine eindeutige Möglichkeit, die Identität des Absenders über die Webinhalte zu bestätigen. Eine Nachricht, die die Webseite empfängt, kann von Ihrer nativen App oder einem anderen iFrame stammen.

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

addJavascriptInterface (Legacy) verwenden

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 der Annotation @JavascriptInterface und fügen dem WebView mit addJavascriptInterface(Object, String) eine Instanz der Klasse hinzu.

Merkmale:

  • 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 der Kotlin- oder Java-Seite erforderlich.
  • Sicherheitsrisiko: Standardmäßig ist addJavascriptInterface für jeden Frame in der WebView verfügbar, auch für iFrames. Es fehlt die ursprungsbasierte Zugriffssteuerung. Aufgrund des asynchronen Verhaltens von WebView ist es nicht möglich, die URL des Frames, der Ihr Interface aufruft, sicher zu ermitteln. 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 nativen Bridge-Implementierungsmechanismen:

Methode addWebMessageListener postWebMessage addJavascriptInterface
Implementierung Asynchron (Listener im Hauptthread) Asynchron Synchron
Sicherheit Höchste (zulassungslistenbasiert) Hoch (ursprungsbezogen) Niedrig (keine Ursprungsüberprüfungen)
Komplexität Moderat Moderat Einfach
Richtung Bidirektional Bidirektional Web-zu-App
WebView-Mindestversion 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 behandelt.

Binärdaten mit Bytearrays übertragen

Mit der Klasse WebMessageCompat 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 diese Methode deutlich speichereffizienter und schneller.

  • Binärvorteil: Binärdaten wie Bild- oder Audiodateien zwischen Ihrer nativen App und Webinhalten übertragen.
  • Einschränkung: Auch bei Byte-Arrays kopiert das System Daten über die IPC-Grenze (Inter-Process Communication) zwischen der App und dem isolierten Prozess, den WebView zum Rendern der Webinhalte verwendet. Bei sehr großen Dateien wird jedoch weiterhin viel Arbeitsspeicher benötigt.

Die folgenden Codebeispiele zeigen, wie Sie addWebMessageListener auf der Seite der nativen App einrichten, um mit WebMessageCompat.TYPE_ARRAY_BUFFER gekennzeichnete Nachrichten zu empfangen 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. Damit können die Webinhalte binäre Daten (ArrayBuffer) über den im vorherigen Beispiel eingefügten window.myBridge-Proxy an die native App senden und von ihr empfangen.

// 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 Methode shouldInterceptRequest, 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.

Dadurch können Daten in Chunks 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 mithilfe eines Standard-fetch()-Aufrufs 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 native App-Seite. Dabei wird die Methode WebViewClient.shouldInterceptRequest in Kotlin und Java verwendet, um eine große Binärdatei zu streamen, indem eine benutzerdefinierte Platzhalter-URL abgefangen wird, die vom Webinhalt angefordert wird.

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

Beachten Sie die folgenden Richtlinien, um Ihre Anwendung und Nutzerdaten zu schützen, wenn Sie eine Bridge implementieren:

  • HTTPS erzwingen: Damit schädliche Inhalte von Drittanbietern nicht die native Logik Ihrer Anwendung aufrufen können, sollten Sie nur die Kommunikation mit sicheren Quellen zulassen.

  • Auf Ursprungsregeln verlassen: Die beste Methode, um mit Vertrauen umzugehen, besteht darin, Ihre allowedOriginRules genau zu definieren und die im Nachrichten-Callback bereitgestellte sourceOrigin zu prüfen. Vermeiden Sie es, den vollständigen Platzhalter (*), der mit allen Ursprüngen übereinstimmt, als einzige Ursprungsregel zu verwenden, es sei denn, dies ist unbedingt erforderlich. Die Verwendung von Platzhaltern für Subdomains (z. B. *.example.com) ist weiterhin gültig und sicher, um mehrere Subdomains (z. B. foo.example.com, bar.example.com) abzugleichen.

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

  • Angriffsfläche minimieren: Geben Sie nur die Methoden oder Daten an, die die Webseite benötigt.

  • Funktionen zur Laufzeit prüfen: Die aktuellen Bridge-APIs, einschließlich addWebMessageListener, sind Teil der Jetpack Webkit-Bibliothek. Prüfen Sie also immer, ob Sie über WebViewFeature.isFeatureSupported() Support erhalten können, bevor Sie anrufen.