Na tej stronie omawiamy różne metody i sprawdzone praktyki tworzenia natywnego mostu, zwanego też mostem JavaScript, który ułatwia komunikację między treściami internetowymi w WebView a aplikacją hostującą na Androida.
Umożliwia to programistom internetowym korzystanie z JavaScriptu w celu uzyskiwania dostępu do funkcji platformy natywnej, takich jak aparat, system plików czy zaawansowane czujniki sprzętowe, których standardowe interfejsy API zwykle nie udostępniają.
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. komunikatów biometrycznych,
BottomSheetDialog) ze strony internetowej. - Wydajność: przenoszenie wymagających obliczeń do natywnego kodu Java lub Kotlin.
- Trwałość danych: dostęp do lokalnych zaszyfrowanych baz danych lub udostępnionych preferencji.
- Przesyłanie dużych ilości danych: przekazywanie plików multimedialnych lub złożonych struktur danych między aplikacją a renderowaniem internetowym.
Mechanizmy komunikacji
Android oferuje 3 główne generacje interfejsów API do tworzenia natywnego pomostu. Wszystkie są nadal dostępne, ale różnią się znacznie pod względem bezpieczeństwa, użyteczności i wydajności.
Użyj addWebMessageListener (zalecane)
addWebMessageListener to najnowocześniejsze i zalecane podejście do komunikacji między treściami internetowymi a kodem aplikacji natywnej. Łączy on łatwość obsługi interfejsu JavaScript z bezpieczeństwem systemu przesyłania wiadomości.
Jak to działa: aplikacja dodaje detektor o określonej nazwie i zestawie reguł dozwolonych źródeł. Komponent WebView dba o to, aby obiekt JavaScript był obecny w zakresie globalnym (window.objectName) od momentu rozpoczęcia wczytywania strony.
Inicjowanie: aby mieć pewność, że komponent WebView wstrzyknie obiekt JavaScript przed uruchomieniem jakiegokolwiek skryptu, musisz wywołać metodę addWebMessageListener przed wywołaniem metody loadUrl().
Najważniejsze funkcje:
Bezpieczeństwo i zaufanie: w przeciwieństwie do starszych interfejsów API ta metoda wymaga
Set<String>allowedOriginRulespodczas inicjowania. To główny mechanizm budowania zaufania.Gdy określisz zaufane źródło, np.
https://example.com, komponent WebView gwarantuje, że udostępni wstrzyknięte obiekty JavaScript tylko stronom internetowym wczytanym z tego źródła.Wywołanie zwrotne odbiornika natywnego otrzymuje parametr
sourceOriginz każdą wiadomością. Możesz użyć tej opcji, aby sprawdzić dokładne pochodzenie nadawcy, jeśli Twój most obsługuje wiele dozwolonych źródeł.Ponieważ komponent WebView ściśle egzekwuje te kontrole pochodzenia na poziomie platformy, aplikacja może ogólnie polegać na komunikatach otrzymywanych z zaufanego źródła
sourceOriginjako prawdziwych, co w większości standardowych implementacji eliminuje potrzebę rygorystycznej weryfikacji ładunku.- WebView dopasowuje reguły do schematu (HTTP/HTTPS), hosta i portu.
- WebView ignoruje ścieżki. Na przykład
https://example.comzezwala nahttps://example.com/loginihttps://example.com/home. - WebView ściśle ogranicza symbole wieloznaczne do początku hosta w przypadku subdomen. Na przykład reguła
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łę pochodzenia 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 działanie mostu do zweryfikowanych domen, co zapobiega wykonywaniu 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ł dotyczących źródła.
Wątkowość: wywołanie zwrotne odbiornika jest wykonywane w głównym wątku aplikacji (UI). Jeśli most musi obsługiwać złożone przetwarzanie danych, analizowanie JSON-a lub wyszukiwanie w bazie danych, musisz przenieść to zadanie do wątku w tle, aby zapobiec zawieszaniu interfejsu aplikacji z błędem „aplikacja nie odpowiada” (ANR).
Dwukierunkowe: gdy strona 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, aby wysyłać dowolną liczbę wiadomości na stronę, a nie tylko odpowiadać na każdą wiadomość wysłaną przez stronę. Jeśli ramka źródłowa zostanie zamknięta lub zniszczona, wiadomości wysłane za pomocą funkcjipostMessage()na serwerze proxy będą 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 załadowaniem strony) lubevaluateJavaScript()(aby ocenić JavaScript po załadowaniu strony).
Ograniczenie: ten interfejs API przesyła dane w postaci ciągów znaków lub tablic 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 poznać pełną sekwencję dwukierunkowej wymiany wiadomości, zdarzenia przebiegają w tej kolejności:
- Inicjowanie (aplikacja): aplikacja natywna rejestruje odbiorcę za pomocą funkcji
addWebMessageListeneri wczytuje stronę internetową za pomocą funkcjiloadUrl(). - Wysłanie wiadomości (internet): JavaScript na stronie internetowej wywołuje
myObject.postMessage(message), aby rozpocząć komunikację. - Odbieranie wiadomości i odpowiadanie na nie (aplikacja): aplikacja odbiera wiadomość w wywołaniu zwrotnym odbiornika i odpowiada na nią za pomocą podanego
replyProxy.postMessage(). - Otrzymanie odpowiedzi (w internecie): strona internetowa otrzymuje odpowiedź asynchroniczną 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 interfejsu addWebMessageListener, który umożliwia treściom internetowym odbieranie wiadomości z aplikacji natywnej i wysyłanie własnych wiadomości za pomocą serwera proxy myObject.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Użyj postWebMessage (alternatywnie)
Android wprowadził to rozwiązanie, aby zapewnić asynchroniczną alternatywę opartą na wiadomościach, podobną do window.postMessage w internecie.
Jak to działa: aplikacja używa WebViewCompat.postWebMessage do wysyłania ładunku do głównej ramki strony internetowej. Aby utworzyć dwukierunkowy kanał komunikacji, możesz utworzyć WebMessageChannel i przekazać jeden z jego portów wraz z wiadomością do treści internetowych.
Cechy:
- Asynchroniczna: podobnie jak
addWebMessageListenerta metoda wykorzystuje asynchroniczne przesyłanie wiadomości, które zapewnia, że strona internetowa pozostaje responsywna na interakcje użytkownika, podczas gdy aplikacja przetwarza dane w tle. - Świadomość pochodzenia: możesz określić
targetOrigin, aby mieć pewność, że komponent WebView przekazuje 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 wczytywanych za pomocą identyfikatorów URI
data:,file:lubloadData(), chyba że jako docelowe źródło podasz „*”. Dzięki temu każda strona może otrzymać wiadomość. - Ryzyko związane z tożsamością: nie ma jasnego sposobu na zweryfikowanie tożsamości nadawcy przez treści internetowe. Wiadomość, którą otrzymuje strona internetowa, może pochodzić z aplikacji natywnej lub innej ramki iframe.
Użyj tej metody, gdy potrzebujesz prostego, asynchronicznego kanału do przesyłania danych w postaci ciągów znaków we wcześniejszych wersjach Androida, które nie obsługują addWebMessageListener.
Używanie addJavascriptInterface (starsza wersja)
Najstarsza metoda polega na wstrzykiwaniu instancji obiektu natywnego bezpośrednio do elementu WebView.
Jak to działa: definiujesz klasę Kotlin lub Java, dodajesz do dozwolonych metod adnotację @JavascriptInterface i dodajesz instancję klasy do WebView za pomocą addJavascriptInterface(Object, String).
Cechy:
- Synchroniczne: środowisko wykonawcze JavaScript blokuje się, dopóki metoda w kodzie Androida nie zwróci wartości.
- Bezpieczeństwo wątków: system wywołuje metody w wątku w tle, co wymaga starannej synchronizacji po stronie Kotlina lub Javy.
- Zagrożenie bezpieczeństwa: domyślnie interfejs
addJavascriptInterfacejest dostępny dla każdej ramki w komponencie WebView, w tym dla ramek iframe. Nie ma kontroli dostępu opartej na pochodzeniu. 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
Tabela poniżej zawiera szybkie porównanie 3 głównych mechanizmów implementacji natywnego pomostu:
| Metoda | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| Implementacja | Asynchroniczne (detektor w wątku głównym) | Asynchroniczny | Synchroniczne |
| Bezpieczeństwo | Najwyższe (na podstawie listy dozwolonych) | Wysoka (z uwzględnieniem źródła) | Niski (bez sprawdzania pochodzenia) |
| Złożoność | Umiarkowana | Umiarkowana | Prosty |
| Kierunek | Dwukierunkowe | Dwukierunkowe | Web to App |
| Minimalna wersja WebView | Wersja 82 (i Jetpack Webkit 1.3.0) | Wersja 45 (i Jetpack Webkit 1.1.0) | Wszystkie wersje |
| Polecane | Tak | Nie | Nie |
Obsługa dużych transferów danych
Podczas przesyłania dużych pakietów danych, takich jak ciągi znaków o rozmiarze wielu megabajtów lub pliki binarne, musisz starannie zarządzać pamięcią, aby uniknąć błędów ANR („Aplikacja nie odpowiada”) lub awarii na urządzeniach 32-bitowych. W tej sekcji omawiamy różne techniki i ograniczenia związane z przesyłaniem dużych 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 Base64. Base64 zwiększa rozmiar danych o około 33%, więc to rozwiązanie jest znacznie bardziej wydajne pod względem pamięci i szybsze.
- Przewaga danych binarnych: przesyłaj dane binarne, takie jak pliki graficzne lub dźwięku, między aplikacją natywną a treściami z internetu.
- Ograniczenie: nawet w przypadku tablic bajtów system kopiuje dane przez granicę komunikacji międzyprocesowej (IPC) między aplikacją a izolowanym procesem, którego 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 w aplikacji natywnej, aby odbierać wiadomości oznaczone symbolem 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ą wstrzykniętego w poprzednim przykładzie serwera proxy window.myBridge.
// 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 (powyżej 10 MB) użyj metody shouldInterceptRequest, aby przesyłać strumieniowo dane:
- Strona internetowa inicjuje wywołanie
fetch()niestandardowego adresu URL zastępczego. Na przykład:https://app.local/large-file. - Aplikacja na Androida przechwytuje to żądanie w funkcji
WebViewClient.shouldInterceptRequest. - Aplikacja zwraca dane w postaci
InputStream.
Umożliwia to przesyłanie strumieniowe danych w częściach zamiast wczytywania całego ładunku do pamięci naraz.
Poniższa funkcja JavaScriptu pokazuje kod po stronie klienta, który umożliwia wydajne wczytywanie dużego pliku binarnego z aplikacji natywnej za pomocą standardowego wywołania fetch() 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ą po stronie aplikacji natywnej, jak za pomocą metody WebViewClient.shouldInterceptRequest w językach Kotlin i Java przesyłać strumieniowo duży plik binarny, przechwytując niestandardowy adres URL zastępczego, o który poprosiła treść internetowa.
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);
}
});
Postępuj zgodnie z zaleceniami dotyczącymi bezpieczeństwa
Aby chronić aplikację i dane użytkowników, podczas wdrażania pomostu postępuj zgodnie z tymi wytycznymi:
Wymagaj protokołu HTTPS: aby mieć pewność, że złośliwe treści pochodzące od osób trzecich nie mogą wywoływać natywnej logiki aplikacji, zezwalaj tylko na komunikację z bezpiecznymi źródłami.
Korzystaj z reguł pochodzenia: najlepszym sposobem na radzenie sobie z zaufaniem jest ścisłe określenie
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 dotyczącej źródła, chyba że jest to bezwzględnie 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: reguły dotyczące pochodzenia chronią przed złośliwymi witrynami innych firm i ukrytymi elementami iframe, ale 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 ataki typu stored XSS, atakujący może wykonać skrypt działający jako zaufane źródło. Rozważ zastosowanie weryfikacji w przypadku ładunków wiadomości przed wykonaniem operacji na platformie natywnej o znaczeniu krytycznym.
Zminimalizuj obszar interfejsu: udostępniaj tylko te metody lub dane, których wymaga strona internetowa.
Sprawdzanie funkcji w czasie działania: najnowsze interfejsy API pomostowe, w tym
addWebMessageListener, są częścią biblioteki Jetpack Webkit. Zanim zadzwonisz do zespołu pomocy, zawsze sprawdź, czy możesz skorzystać zWebViewFeature.isFeatureSupported().