Na tej stronie omawiamy różne metody i sprawdzone sposoby tworzenia mostu natywnego, zwanego też mostem JavaScript, który ułatwia komunikację między treściami z internetu w komponencie WebView a aplikacją na Androida.
Dzięki temu programiści stron internetowych mogą używać JavaScriptu do uzyskiwania dostępu do funkcji platformy natywnej, takich jak aparat, system plików czy zaawansowane czujniki sprzętowe, które nie są zwykle dostępne w standardowych interfejsach API.
Przypadki użycia
Implementacja mostu JavaScript umożliwia różne scenariusze integracji, w których treści internetowe wymagają głębszego dostępu do systemu operacyjnego Android. Oto kilka przykładów:
- Integracja z platformą: wywoływanie natywnych komponentów interfejsu Androida (np.
promptów biometrycznych,
BottomSheetDialog) ze strony internetowej. - Wydajność: przenoszenie złożonych zadań obliczeniowych do natywnego kodu Java lub Kotlin.
- Trwałość danych: dostęp do lokalnych zaszyfrowanych baz danych lub ustawień współdzielonych.
- Przesyłanie dużych ilości danych: przekazywanie plików multimedialnych lub złożonych struktur danych między aplikacją a rendererem internetowym.
Mechanizmy komunikacji
Android oferuje 3 główne generacje interfejsów API do tworzenia mostu natywnego. Wszystkie są nadal dostępne, ale znacznie się od siebie różnią pod względem bezpieczeństwa, użyteczności i wydajności.
Używanie addWebMessageListener (zalecane)
addWebMessageListener to najnowocześniejsze i zalecane podejście do komunikacji między treściami internetowymi a kodem aplikacji natywnej. Łączy łatwość użycia interfejsu JavaScript z bezpieczeństwem systemu przesyłania wiadomości.
Jak to działa: aplikacja dodaje odbiornik o określonej nazwie i zestawie
dozwolonych reguł źródła. Następnie komponent WebView zapewnia, że obiekt JavaScript jest obecny w zakresie globalnym (window.objectName) od momentu rozpoczęcia wczytywania strony.
Inicjowanie: aby mieć pewność, że komponent WebView wstrzyknie obiekt JavaScript przed
uruchomieniem skryptu, musisz wywołać addWebMessageListener przed przejściem na
stronę (np. wywołując WebViewCompat.navigate lub loadUrl).
Najważniejsze funkcje:
Bezpieczeństwo i zaufanie: w przeciwieństwie do starszych interfejsów API ta metoda wymaga podczas inicjowania
Set<String>allowedOriginRules. Jest to podstawowy mechanizm budowania zaufania.Gdy określisz zaufane źródło, np.
https://example.com, komponent WebView gwarantuje, że wstrzyknięte obiekty JavaScript będą udostępniane tylko stronom internetowym wczytanym z tego źródła.Wywołanie zwrotne odbiornika natywnego otrzymuje z każdą wiadomością parametr
sourceOrigin. Możesz go użyć do sprawdzenia dokładnego źródła nadawcy, jeśli Twój most obsługuje wiele dozwolonych źródeł.Ponieważ komponent WebView ściśle egzekwuje te kontrole źródła na poziomie platformy, Twoja aplikacja może na ogół polegać na wiadomościach otrzymywanych z zaufanego źródła
sourceOriginjako prawdziwych, co eliminuje potrzebę rygorystycznej weryfikacji ładunku w większości standardowych implementacji.- Komponent WebView dopasowuje reguły do schematu (HTTP/HTTPS), hosta i portu.
- Komponent WebView ignoruje ścieżki. Na przykład
https://example.comzezwala nahttps://example.com/loginihttps://example.com/home. - Komponent WebView ściśle ogranicza symbole wieloznaczne do początku hosta w przypadku subdomen. Na przykład
https://*.example.compasuje dohttps://foo.example.com, ale nie dohttps://example.com. Jeśli chcesz dopasować zarównohttps://example.com, jak i jego subdomeny, musisz dodać każdą regułę źródła osobno do listy dozwolonych (np."https://example.com", "https://*.example.com"). Nie możesz używać symboli wieloznacznych w schemacie ani w środku domeny.
Ogranicza to most do zweryfikowanych domen, co uniemożliwia wykonywanie kodu natywnego przez nieautoryzowane treści osób trzecich lub wstrzyknięte elementy iframe.
Obsługa wielu ramek: działa we wszystkich ramkach, które pasują do reguł źródła.
Wątki: wywołanie zwrotne odbiornika jest wykonywane w głównym wątku aplikacji (UI) . Jeśli Twój most musi obsługiwać złożone przetwarzanie danych, analizowanie JSON lub wyszukiwanie w bazie danych, musisz przenieść to zadanie do wątku w tle, aby zapobiec zawieszaniu się interfejsu aplikacji z powodu błędu „aplikacja nie odpowiada” (ANR).
Dwukierunkowe: gdy strona internetowa wysyła wiadomość, aplikacja otrzymuje
JavaScriptReplyProxy, którego może użyć do wysyłania wiadomości z powrotem do tej konkretnej ramki. Możesz zachować ten obiektreplyProxyi używać go w dowolnym momencie do wysyłania dowolnej liczby wiadomości do strony, a nie tylko do odpowiadania na każdą wiadomość wysyłaną przez stronę. Jeśli ramka źródłowa zostanie zamknięta lub zniszczona, wiadomości wysyłane za pomocąpostMessage()w proxy są ignorowane.Inicjowanie po stronie aplikacji: chociaż strona internetowa musi zawsze inicjować kanał komunikacji z aplikacją, aplikacja natywna może jednostronnie poprosić stronę internetową o rozpoczęcie tego procesu. Aplikacja natywna może komunikować się ze stroną internetową za pomocą
addDocumentStartJavaScript()(aby ocenić JavaScript przed wczytaniem strony) lubevaluateJavaScript()(aby ocenić JavaScript po wczytaniu strony).
Ograniczenie: ten interfejs API wysyła dane jako ciągi znaków lub tablice byte[]. W przypadku bardziej złożonych struktur danych, takich jak obiekty JSON, musisz je serializować do jednego z tych formatów, a następnie deserializować po drugiej stronie, aby odtworzyć strukturę danych.
Przykład użycia:
Aby zrozumieć pełną sekwencję dwukierunkowej wymiany wiadomości, zdarzenia przebiegają w tej kolejności:
- Inicjowanie (aplikacja): aplikacja natywna rejestruje odbiornik za pomocą
addWebMessageListeneri inicjuje nawigację po stronie (np. za pomocąWebViewCompat.navigatelubloadUrl). - Wysyłanie wiadomości (strona internetowa): JavaScript strony internetowej wywołuje
myObject.postMessage(message), aby zainicjować komunikację. - Odbieranie wiadomości i odpowiadanie na nią (aplikacja): aplikacja odbiera wiadomość w wywołaniu zwrotnym
odbiornika i odpowiada za pomocą podanego
replyProxy.postMessage(). - Odbieranie odpowiedzi (strona internetowa): strona internetowa odbiera asynchroniczną odpowiedź w funkcji wywołania zwrotnego
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);
}
Poniższy kod JavaScript pokazuje implementację po stronie klienta funkcji addWebMessageListener, która umożliwia treściom internetowym odbieranie wiadomości z aplikacji natywnej i wysyłanie własnych wiadomości za pomocą proxy myObject.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Używanie postWebMessage (alternatywa)
Android wprowadził tę metodę, aby zapewnić asynchroniczną alternatywę opartą na przesyłaniu wiadomości, podobną do window.postMessage w internecie.
Jak to działa: aplikacja używa WebViewCompat.postWebMessage, aby wysłać ładunek
do głównej ramki strony internetowej. Aby utworzyć dwukierunkowy kanał komunikacji, możesz utworzyć WebMessageChannel i przekazać jeden z jego
portów z wiadomością do treści internetowych.
Cechy:
- Asynchroniczne: podobnie jak
addWebMessageListener, ta metoda używa asynchronicznego przesyłania wiadomości, co zapewnia, że strona internetowa pozostaje responsywna na interakcje użytkownika, podczas gdy aplikacja przetwarza dane w tle. - Świadomość źródła: możesz określić
targetOrigin, aby mieć pewność, że komponent WebView dostarcza dane tylko do zaufanej witryny.
Ograniczenia:
- Zakres: ten interfejs API ogranicza komunikację do głównej ramki. Nie obsługuje bezpośredniego adresowania ani wysyłania wiadomości do elementów iframe.
- Ograniczenia dotyczące identyfikatorów URI: nie możesz używać tej metody w przypadku treści wczytanych za pomocą identyfikatorów URI
data:,file:lubloadData(), chyba że jako źródło docelowe określisz „*”. Dzięki temu każda strona może otrzymać wiadomość. - Ryzyko związane z tożsamością: treści internetowe nie mają jasnego sposobu na sprawdzenie tożsamości nadawcy. Wiadomość, którą otrzymuje strona internetowa, może pochodzić z Twojej aplikacji natywnej lub innego elementu iframe.
Użyj tej metody, gdy potrzebujesz prostego, asynchronicznego kanału do przesyłania danych opartych na ciągach znaków w starszych wersjach Androida, które nie obsługują addWebMessageListener.
Używanie addJavascriptInterface (starsza wersja)
Najstarsza metoda polega na wstrzyknięciu instancji obiektu natywnego bezpośrednio do komponentu WebView.
Jak to działa: definiujesz klasę Kotlin lub Java, dodajesz dozwolone
metody za pomocą adnotacji @JavascriptInterface i dodajesz instancję klasy do
komponentu WebView za pomocą addJavascriptInterface(Object, String).
Cechy:
- Synchroniczne: środowisko wykonawcze JavaScript jest blokowane do momentu, aż metoda w kodzie Androida zwróci wartość.
- Bezpieczeństwo wątków: system wywołuje metody w wątku w tle, co wymaga starannej synchronizacji po stronie Kotlin lub Java.
- Ryzyko związane z bezpieczeństwem: domyślnie
addJavascriptInterfacejest dostępne dla każdej ramki w komponencie WebView, w tym dla elementów iframe. Nie ma kontroli dostępu opartej na źródle. Ze względu na asynchroniczne działanie komponentu WebView nie można bezpiecznie określić adresu URL ramki, która wywołuje Twój interfejs. Nie możesz polegać na metodach takich jakWebView.getUrl()w celu weryfikacji bezpieczeństwa , ponieważ nie gwarantują one dokładności i nie wskazują , która konkretna ramka wysłała żądanie.
Podsumowanie mechanizmów
W tabeli poniżej znajdziesz szybkie porównanie 3 głównych mechanizmów implementacji mostu natywnego:
| Metoda | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| Implementacja | Asynchroniczna (odbiornik w głównym wątku) | Asynchroniczna | Synchroniczna |
| Bezpieczeństwo | Najwyższe (oparte na liście dozwolonych) | Wysokie (świadomość źródła) | Niskie (brak sprawdzania źródła) |
| Złożoność | Umiarkowana | Umiarkowana | Prosta |
| Kierunek | Dwukierunkowe | Dwukierunkowe | Od strony internetowej do aplikacji |
| Minimalna wersja komponentu WebView | Wersja 82 (i Jetpack Webkit 1.3.0) | Wersja 45 (i Jetpack Webkit 1.1.0) | Wszystkie wersje |
| Zalecane | Tak | Nie | Nie |
Obsługa przesyłania dużych ilości danych
Podczas przesyłania dużych ładunków, takich jak ciągi znaków lub pliki binarne o rozmiarze wielu megabajtów, musisz starannie zarządzać pamięcią, aby uniknąć błędów „aplikacja nie odpowiada” (ANR) lub awarii na urządzeniach 32-bitowych. W tej sekcji omawiamy różne techniki i ograniczenia związane z przesyłaniem znacznych ilości danych między aplikacją hosta a treściami internetowymi.
Przesyłanie danych binarnych za pomocą tablic bajtów
Za pomocą klasy WebMessageCompat możesz wysyłać tablice byte[] bezpośrednio
zamiast serializować dane binarne do ciągów znaków Base64. Ponieważ Base64 dodaje do rozmiaru danych około 33% narzutu, jest to znacznie bardziej wydajne i szybsze.
- Zalety danych binarnych: przesyłaj dane binarne, takie jak pliki graficzne lub dźwiękowe, między aplikacją natywną a treściami internetowymi.
- Ograniczenie: nawet w przypadku tablic bajtów system kopiuje dane przez granicę komunikacji międzyprocesowej (IPC) między aplikacją a izolowanym procesem, którego komponent WebView używa do renderowania treści internetowych. W przypadku bardzo dużych plików nadal zużywa to znaczną ilość pamięci.
Poniższe przykłady kodu pokazują, jak skonfigurować addWebMessageListener po stronie aplikacji natywnej, aby odbierać wiadomości oznaczone jako WebMessageCompat.TYPE_ARRAY_BUFFER i opcjonalnie odpowiadać danymi binarnymi, sprawdzając 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
);
}
}
Poniższy kod JavaScript pokazuje implementację po stronie klienta funkcji addWebMessageListener, która umożliwia treściom internetowym wysyłanie i odbieranie danych binarnych (ArrayBuffer) do i z aplikacji natywnej za pomocą proxy window.myBridge wstrzykniętego w poprzednim przykładzie.
// 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]);
}
};
}
Wydajne wczytywanie danych na dużą skalę
W przypadku bardzo dużych plików (>10 MB) użyj metody shouldInterceptRequest, aby
przesyłać dane strumieniowo:
- Strona internetowa inicjuje wywołanie
fetch()do niestandardowego adresu URL zastępczego. Na przykładhttps://app.local/large-file. - Aplikacja na Androida przechwytuje to żądanie w
WebViewClient.shouldInterceptRequest. - Aplikacja zwraca dane jako
InputStream.
Umożliwia to przesyłanie danych strumieniowo w częściach, a nie wczytywanie całego ładunku do pamięci naraz.
Poniższa funkcja JavaScript pokazuje kod po stronie klienta, który umożliwia wydajne wczytywanie dużego pliku binarnego z aplikacji natywnej za pomocą standardowego wywołania fetch() do niestandardowego adresu URL zastępczego.
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);
}
}
Poniższe przykłady kodu pokazują, jak po stronie aplikacji natywnej użyć metody WebViewClient.shouldInterceptRequest w Kotlinie i Javie, aby przesyłać strumieniowo duży plik binarny przez przechwytywanie niestandardowego adresu URL zastępczego żądanego przez treści internetowe.
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);
}
});
Stosowanie się do zaleceń dotyczących bezpieczeństwa
Aby chronić aplikację i dane użytkowników, podczas implementowania mostu postępuj zgodnie z tymi wytycznymi:
Wymuszaj HTTPS: aby mieć pewność, że złośliwe treści osób trzecich nie mogą wywoływać natywnej logiki aplikacji, zezwalaj na komunikację tylko z bezpiecznymi źródłami.
Polegaj na regułach źródła: najlepszym sposobem na radzenie sobie z zaufaniem jest ścisłe zdefiniowanie
allowedOriginRulesi sprawdzeniesourceOriginpodanego w wywołaniu zwrotnym wiadomości. Unikaj używania pełnego symbolu wieloznacznego (*), który pasuje do wszystkich źródeł, jako jedynej reguły źródła, chyba że jest to absolutnie konieczne. Używanie symboli wieloznacznych w przypadku subdomen (np.*.example.com) pozostaje prawidłowe i bezpieczne w przypadku dopasowywania wielu subdomen (np.foo.example.com,bar.example.com).Uwaga: chociaż reguły źródła chronią przed złośliwymi witrynami innych firm i ukrytymi elementami iframe, nie chronią przed lukami w zabezpieczeniach typu cross-site scripting (XSS) w Twojej zaufanej domenie. Jeśli na przykład Twoja strona internetowa wyświetla treści użytkowników i jest podatna na przechowywane XSS, osoba przeprowadzająca atak może uruchomić skrypt działający jako Twoje zaufane źródło. Zanim wykonasz operacje na platformie natywnej, rozważ zastosowanie weryfikacji do ładunków wiadomości.
Minimalizuj obszar ataku: udostępniaj tylko te metody lub dane, których wymaga strona internetowa.
Sprawdzaj funkcje w czasie działania: najnowsze interfejsy API mostu, w tym
addWebMessageListener, są częścią biblioteki Jetpack Webkit. Dlatego zawsze sprawdzaj, czy są obsługiwane, za pomocąWebViewFeature.isFeatureSupported()przed ich wywołaniem.