Questa pagina illustra i vari metodi e le best practice per stabilire un bridge nativo, noto anche come bridge JavaScript, per facilitare la comunicazione tra i contenuti web in un WebView e un'applicazione Android host.
In questo modo, gli sviluppatori web possono utilizzare JavaScript per accedere alle funzionalità della piattaforma nativa, come la fotocamera, il file system o i sensori hardware avanzati, che le API web standard normalmente non forniscono.
Casi d'uso
Un'implementazione del bridge JavaScript consente vari scenari di integrazione in cui i contenuti web richiedono un accesso più approfondito al sistema operativo Android. Di seguito sono riportati alcuni esempi:
- Integrazione della piattaforma: attivazione di componenti UI Android nativi (ad esempio, prompt biometrici,
BottomSheetDialog) da una pagina web. - Rendimento: delega di attività di calcolo pesanti a codice Java o Kotlin nativo.
- Persistenza dei dati: accesso a database criptati locali o preferenze condivise.
- Trasferimenti di grandi quantità di dati: passaggio di file multimediali o strutture di dati complesse tra l'app e il renderer web.
Meccanismi di comunicazione
Android offre tre generazioni principali di API per stabilire un bridge nativo. Sebbene siano ancora tutti disponibili, differiscono in modo significativo per sicurezza, usabilità e prestazioni.
Utilizza addWebMessageListener (consigliato)
addWebMessageListener è l'approccio più moderno e consigliato per la comunicazione tra i contenuti web e il codice dell'app nativa. Combina la facilità
d'uso dell'interfaccia JavaScript con la sicurezza del sistema di messaggistica.
Come funziona: l'app aggiunge un listener con un nome specifico e un insieme di regole di origine consentite. Il componente WebView garantisce quindi che l'oggetto JavaScript sia
presente nell'ambito globale (window.objectName) dal momento in cui la pagina
inizia a caricarsi.
Inizializzazione: per assicurarti che il componente WebView inserisca l'oggetto JavaScript prima dell'esecuzione di qualsiasi script, devi chiamare addWebMessageListener prima di chiamare loadUrl().
Funzionalità principali:
Sicurezza e affidabilità: a differenza delle API legacy, questo metodo richiede un
Set<String>diallowedOriginRulesdurante l'inizializzazione. Questo è il meccanismo principale per stabilire la fiducia.Quando specifichi un'origine attendibile, ad esempio
https://example.com, il componente WebView garantisce che esponga gli oggetti JavaScript inseriti solo alle pagine web caricate da quell'origine esatta.Il callback del listener nativo riceve un parametro
sourceOrigincon ogni messaggio. Puoi utilizzarlo per verificare l'origine esatta del mittente se il tuo bridge supporta più origini consentite.Poiché WebView applica rigorosamente questi controlli dell'origine a livello di piattaforma, la tua app può generalmente fare affidamento sui messaggi ricevuti da un
sourceOriginattendibile come veritieri, eliminando la necessità di una rigorosa convalida del payload nella maggior parte delle implementazioni standard.- WebView confronta le regole con lo schema (HTTP/HTTPS), l'host e la porta.
- WebView ignora i percorsi. Ad esempio,
https://example.comconsentehttps://example.com/loginehttps://example.com/home. - WebView limita rigorosamente i caratteri jolly all'inizio dell'host per
i sottodomini. Ad esempio,
https://*.example.comcorrisponde ahttps://foo.example.com, ma non ahttps://example.com. Se devi corrispondere sia ahttps://example.comsia ai relativi sottodomini, devi aggiungere ogni regola di origine separatamente all'elenco consentito (ad esempio,"https://example.com", "https://*.example.com"). Non puoi utilizzare caratteri jolly per lo schema o al centro di un dominio.
In questo modo, il bridge è limitato ai domini verificati, impedendo l'esecuzione di codice nativo da parte di contenuti di terze parti non autorizzati o iframe inseriti.
Supporto multi-frame: funziona su tutti i frame che corrispondono alle regole di origine.
Threading: il callback del listener viene eseguito sul thread principale (UI) dell'applicazione. Se il bridge deve gestire l'elaborazione di dati complessi, l'analisi JSON o le ricerche nel database, devi scaricare il lavoro su un thread in background per evitare il blocco dell'interfaccia utente dell'applicazione con un errore "L'app non risponde" (ANR).
Bidirezionale: quando la pagina web invia un messaggio, l'app riceve un
JavaScriptReplyProxyche può utilizzare per inviare messaggi a quel frame specifico. Puoi conservare questo oggettoreplyProxye utilizzarlo in qualsiasi momento per inviare un numero qualsiasi di messaggi alla pagina, non solo per rispondere a ogni singolo messaggio inviato dalla pagina. Se il frame di origine esce dalla navigazione o viene eliminato, i messaggi inviati utilizzandopostMessage()sul proxy vengono ignorati automaticamente.Avvio lato app: anche se la pagina web deve sempre avviare il canale di comunicazione con l'app, l'app nativa può richiedere unilateralmente alla pagina web di iniziare questa procedura. L'app nativa può comunicare con la pagina web con
addDocumentStartJavaScript()(per valutare JavaScript prima del caricamento della pagina) oevaluateJavaScript()(per valutare JavaScript dopo il caricamento della pagina).
Limitazione: questa API invia i dati come stringhe o array byte[]. Per
strutture di dati più complesse, ad esempio oggetti JSON, devi serializzarle
in uno di questi formati e poi deserializzarle dall'altra parte per ricostruire
la struttura dei dati.
Esempio di utilizzo:
Per comprendere la sequenza completa di uno scambio di messaggi bidirezionale, gli eventi si svolgono in questo ordine:
- Avvio (app): l'app nativa registra il listener con
addWebMessageListenere carica la pagina web conloadUrl(). - Invio di messaggi (web): le chiamate JavaScript della pagina web
myObject.postMessage(message)per avviare la comunicazione. - Ricezione e risposta ai messaggi (app): l'app riceve il messaggio nel
callback del listener e risponde utilizzando
replyProxy.postMessage()fornito. - Ricezione risposta (web): la pagina web riceve la risposta asincrona nella
funzione di callback
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);
}
Il seguente codice JavaScript mostra l'implementazione lato client di
addWebMessageListener, consentendo ai contenuti web di ricevere messaggi dall'app
nativa e di inviare i propri messaggi tramite il proxy myObject.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Usa postWebMessage (alternativa)
Android ha introdotto questa funzionalità per fornire un'alternativa asincrona basata sulla messaggistica
simile a window.postMessage del web.
Come funziona: l'app utilizza WebViewCompat.postWebMessage per inviare un payload
al frame principale della pagina web. Per stabilire un canale di comunicazione bidirezionale, puoi creare un WebMessageChannel e passare una delle sue porte con il messaggio ai contenuti web.
Caratteristiche:
- Asincrono: come
addWebMessageListener, questo metodo utilizza la messaggistica asincrona, che garantisce che la pagina web rimanga reattiva alle interazioni degli utenti mentre l'app elabora i dati in background. - Riconoscimento dell'origine: puoi specificare un
targetOriginper assicurarti che WebView fornisca i dati solo a un sito web attendibile.
Limitazioni:
- Ambito: questa API limita la comunicazione al frame principale. Non supporta l'indirizzamento diretto o l'invio di messaggi agli iframe.
- Limitazioni URI: non puoi utilizzare questo metodo per i contenuti caricati utilizzando
URI
data:, URIfile:oloadData(), a meno che tu non specifichi "*" come origine di destinazione. In questo modo, qualsiasi pagina può ricevere il messaggio. - Rischio di identità: non esiste un modo chiaro per i contenuti web di verificare l'identità del mittente. Un messaggio ricevuto dalla pagina web potrebbe provenire dalla tua app nativa o da un altro iframe.
Utilizza questo metodo quando hai bisogno di un canale asincrono semplice per i dati basati su stringhe nelle
versioni precedenti di Android che non supportano addWebMessageListener.
Utilizza addJavascriptInterface (legacy)
Il metodo più vecchio prevede l'inserimento di un'istanza di oggetto nativa direttamente nella WebView.
Come funziona: definisci una classe Kotlin o Java, annota i metodi consentiti con @JavascriptInterface e aggiungi un'istanza della classe a WebView utilizzando addJavascriptInterface(Object, String).
Caratteristiche:
- Sincrono: l'ambiente di esecuzione JavaScript viene bloccato finché il metodo nel codice Android non restituisce un valore.
- Thread safety: il sistema chiama i metodi su un thread in background, richiedendo una sincronizzazione accurata sul lato Kotlin o Java.
- Rischio per la sicurezza: per impostazione predefinita,
addJavascriptInterfaceè disponibile per ogni frame all'interno di WebView, inclusi gli iframe. Manca il controllo dell'accesso basato sull'origine. A causa del comportamento asincrono di WebView, non è possibile determinare in modo sicuro l'URL del frame che chiama la tua interfaccia. Non devi fare affidamento su metodi comeWebView.getUrl()per la verifica della sicurezza, in quanto non è garantita la loro accuratezza e non indicano quale frame specifico ha effettuato la richiesta.
Riepilogo dei meccanismi
La seguente tabella fornisce un rapido confronto dei tre meccanismi di implementazione del bridge nativo principali:
| Metodo | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| Implementazione | Asincrono (listener sul thread principale) | Asincrono | Sincrono |
| Sicurezza | Massimo (in base alla lista consentita) | Elevata (con riconoscimento dell'origine) | Basso (nessun controllo dell'origine) |
| Complessità | Moderata | Moderata | Semplice |
| Direzione | Bidirezionale | Bidirezionale | Web to app |
| Versione minima di WebView | Versione 82 (e Jetpack Webkit 1.3.0) | Versione 45 (e Jetpack Webkit 1.1.0) | Tutte le versioni |
| Consigliati | Sì | No | No |
Gestire grandi trasferimenti di dati
Devi gestire attentamente la memoria quando trasferisci payload di grandi dimensioni, ad esempio stringhe di più megabyte o file binari, per evitare errori "L'applicazione non risponde" (ANR) o arresti anomali sui dispositivi a 32 bit. Questa sezione descrive le varie tecniche e limitazioni associate al trasferimento di grandi quantità di dati tra l'applicazione host e i contenuti web.
Trasferire dati binari con array di byte
Con la classe WebMessageCompat, puoi inviare direttamente array byte[]
anziché serializzare i dati binari in stringhe Base64. Poiché Base64 aggiunge
un sovraccarico di circa il 33% alle dimensioni dei dati, questo metodo è molto più
efficiente in termini di memoria e più veloce.
- Vantaggio binario: trasferisci dati binari come file immagine o audio tra la tua app nativa e i contenuti web.
- Limitazione: anche con gli array di byte, il sistema copia i dati oltre il limite di comunicazione interprocesso (IPC) tra l'app e il processo isolato che WebView utilizza per il rendering dei contenuti web. Questa operazione consuma comunque una quantità significativa di memoria per i file molto grandi.
I seguenti esempi di codice mostrano come configurare addWebMessageListener sul lato dell'app nativa per ricevere messaggi contrassegnati con WebMessageCompat.TYPE_ARRAY_BUFFER e, facoltativamente, rispondere con dati binari controllando 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
);
}
}
Il seguente codice JavaScript mostra l'implementazione lato client di
addWebMessageListener, consentendo ai contenuti web di inviare e ricevere dati
binari (ArrayBuffer) da e verso l'app nativa utilizzando il proxy window.myBridge
inserito nell'esempio precedente.
// 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]);
}
};
}
Caricamento efficiente di dati su larga scala
Per file molto grandi (> 10 MB), utilizza il metodo shouldInterceptRequest per
trasmettere i dati in streaming:
- La pagina web avvia una chiamata
fetch()a un URL personalizzato e segnaposto. Ad esempio,https://app.local/large-file. - L'app per Android intercetta questa richiesta in
WebViewClient.shouldInterceptRequest. - L'app restituisce i dati come
InputStream.
Ciò consente di trasmettere i dati in streaming in blocchi anziché caricare l'intero payload in memoria contemporaneamente.
La seguente funzione JavaScript mostra il codice lato client per
caricare in modo efficiente un file binario di grandi dimensioni dall'applicazione nativa utilizzando una
chiamata fetch() standard a un URL segnaposto personalizzato.
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);
}
}
I seguenti esempi di codice mostrano il lato dell'app nativa, utilizzando il metodo WebViewClient.shouldInterceptRequest sia in Kotlin che in Java, per trasmettere in streaming un file binario di grandi dimensioni intercettando un URL segnaposto personalizzato richiesto dai contenuti 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);
}
});
Seguire i consigli per la sicurezza
Per proteggere l'applicazione e i dati degli utenti, segui queste linee guida quando implementi un bridge:
Applica HTTPS: per assicurarti che i contenuti dannosi di terze parti non possano richiamare la logica nativa della tua applicazione, consenti solo la comunicazione con origini sicure.
Affidati alle regole di origine: il modo migliore per gestire l'attendibilità è definire rigorosamente il tuo
allowedOriginRulese controllare ilsourceOriginfornito nel callback del messaggio. Evita di utilizzare il carattere jolly completo (*), che corrisponde a tutte le origini, come unica regola di origine, a meno che non sia assolutamente necessario. L'utilizzo di caratteri jolly per i sottodomini (ad esempio,*.example.com) rimane valido e sicuro per la corrispondenza con più sottodomini (ad esempio,foo.example.com,bar.example.com).Nota: mentre le regole di origine proteggono da siti web di terze parti dannosi e da iframe nascosti, non possono proteggere dalle vulnerabilità cross-site scripting (XSS) all'interno del tuo dominio attendibile. Ad esempio, se la tua pagina web mostra contenuti generati dagli utenti ed è vulnerabile a XSS memorizzato, un malintenzionato potrebbe eseguire uno script che funge da origine attendibile. Valuta la possibilità di applicare la convalida ai payload dei messaggi prima di eseguire operazioni sensibili della piattaforma nativa.
Ridurre al minimo la superficie di attacco: esponi solo i metodi o i dati specifici richiesti dalla pagina web.
Controlla le funzionalità in fase di runtime: le API bridge recenti, tra cui
addWebMessageListener, fanno parte della libreria Jetpack Webkit. Pertanto, controlla sempre se è disponibile l'assistenza tramiteWebViewFeature.isFeatureSupported()prima di chiamare.