WebView uruchamia kod natywny w wielu procesach, aby renderować treści z internetu w aplikacji na Androida. Pozostawienie instancji WebView bez zarządzania może prowadzić do wycieków pamięci, awarii spowodowanych brakiem pamięci (OOM) i obniżenia wydajności aplikacji.
W tym dokumencie wyjaśniamy model pamięci wieloprocesowej WebView, opisujemy, jak prawidłowo zarządzać jej cyklem życia, aby zapobiegać wyciekom, oraz przedstawiamy praktyczne przepływy pracy do diagnozowania problemów z pamięcią.
Architektura pamięci WebView
Aby skutecznie zarządzać pamięcią WebView, dowiedz się, jak Android przydziela zasoby na potrzeby treści z internetu:
Wykonywanie wieloprocesowe: na Androidzie 8.0 (poziom interfejsu API 26) i nowszych
WebViewoddziela treści z internetu od podstawowych funkcji aplikacji w ramach wielu procesów (na urządzeniach z małą ilością pamięci RAM może to być 1 proces):- Proces hosta (przeglądarki): główny proces aplikacji, w którym działa kod
Activityoraz kod Java lub Kotlin. - Izolowany proces renderowania: osobny proces w piaskownicy (
SandboxedProcessService), który analizuje kod HTML i CSS, wykonuje kod JavaScript i renderuje strony internetowe.
- Proces hosta (przeglądarki): główny proces aplikacji, w którym działa kod
Wykorzystanie pamięci natywnej: większość pamięci
WebView, w tym wyrenderowana grafika, drzewo DOM i pamięć środowiska wykonawczego JavaScriptu, jest przydzielana w pamięci natywnej, a nie na stercie Javy. Zrzut stosu Java (.hprof) pokazuje tylko lekki obiekt opakowujący Java i nie rejestruje rzeczywistej pamięci używanej przez treści z internetu.Wpływ pamięci natywnej na system: w przeciwieństwie do alokacji sterty Javy, które są ograniczone przez limit
maxHeapaplikacji i szybko kończą się błędemOutOfMemoryError, pamięć natywna może niezauważalnie wzrosnąć do gigabajtów. Gdy niezwolniona pamięć natywna zapełnia fizyczną pamięć RAM i przestrzeń wymiany (zRAM), mechanizm Low Memory Killer (LMK) Androida zaczyna kończyć procesy w tle, aby odzyskać pamięć. Pogarsza to ogólną wielozadaniowość urządzenia, zanim ostatecznie zamknie aplikację na pierwszym planie.
Zarządzanie cyklem życia WebView
Prawidłowe zarządzanie cyklem życia ma kluczowe znaczenie dla zapobiegania wyciekom pamięci. Częstym błędem jest zakładanie, że usunięcie elementu WebView z układu lub zakończenie działania elementu Activity automatycznie zwalnia pamięć.
Aby zapewnić całkowite wyczyszczenie zarówno odwołań do kontekstu Javy, jak i natywnych zasobów renderowania, musisz wyraźnie zaplanować sekwencję usuwania w cyklu życia komponentu hosta (np. onDestroy()), zatrzymując wykonywanie aktywnej strony, odłączając widok od kontenera i zwalniając natywne powiązania.
Czyszczenie instancji WebView
Aby zapewnić prawidłowe wyłączenie i zwolnienie zasobów po zniszczeniu Activity lub Fragment, wykonaj te czynności:
- Usuń element
WebViewz kontenera nadrzędnego (ViewGroup). - Zatrzymaj aktywne wczytywanie i wyczyść historię nawigacji.
- Zadzwoń:
destroy() - Usuń odniesienie do
null.
Poniższy przykład pokazuje, jak prawidłowo wyczyścić obiekt WebView:
Kotlin
override fun onDestroy() { myWebView?.let { // Remove the WebView from its parent ViewGroup. (it.parent as? ViewGroup)?.removeView(it) // Stop active loading and clear history. it.stopLoading() it.clearHistory() // Destroy the instance. it.destroy() } myWebView = null super.onDestroy() }
Java
@Override
protected void onDestroy() {
if (myWebView != null) {
// Remove the WebView from its parent ViewGroup.
if (myWebView.getParent() instanceof ViewGroup) {
((ViewGroup) myWebView.getParent()).removeView(myWebView);
}
// Stop active loading and clear history.
myWebView.stopLoading();
myWebView.clearHistory();
// Destroy the instance.
myWebView.destroy();
}
myWebView = null;
super.onDestroy();
}
Pamięć po zniszczeniu
Gdy wywołasz metodę destroy(), system zwolni kontekst Activity, wyczyści hierarchie widoków i zatrzyma działanie w tle w internecie. Możesz jednak zauważyć, że pamięć fizyczna procesu (rozmiar zbioru roboczego) nie spada od razu do poziomu bazowego sprzed WebView.
Jest to normalne zachowanie. Pamięci podręczne środowiska wykonawczego, biblioteki udostępnione i przydzielone strony pamięci pozostają w procesie, dopóki system operacyjny ich nie odzyska lub proces nie zostanie zakończony. Głównym celem destroy() jest zapobieganie skumulowanym wyciekom pamięci Activity, gdy użytkownicy przechodzą do ekranów internetowych i z nich wychodzą.
Kluczowe dane debugowania
Podczas analizowania wykorzystania pamięci przez WebView skup się na tych danych:
Resident Set Size (RSS): łączna ilość fizycznej pamięci RAM zmapowanej na proces, w tym udostępniony kod i biblioteki (oznaczona jako Łącznie w Android Studio Profiler).
Anonimowy RSS (RssAnon): pamięć przydzielona bezpośrednio przez proces, która nie jest powiązana z plikiem na dysku (np. sterta natywna i alokacje środowiska wykonawczego JavaScriptu). Reprezentuje to główny koszt pamięci treści z internetu (oznaczony jako Przydzielone w Profilerze Android Studio).
Wykorzystanie pamięci prywatnej (PMF): suma anonimowej pamięci RSS i przestrzeni wymiany (zRAM). PMF odzwierciedla rzeczywiste obciążenie pamięci, które aplikacja nakłada na system i którego nie można usunąć.
PMF przeglądarki a PMF mechanizmu renderowania: pamięć używana przez główny proces aplikacji w porównaniu z pamięcią używaną przez odizolowany proces mechanizmu renderowania. Ciężkie treści z internetu powodują skoki głównie w procesie mechanizmu renderowania.
Liczba aktywnych obiektów (
WebViews,Activities,Views): liczba aktywnych instancji interfejsu, kontekstu iWebViewprzechowywanych w pamięci. Śledzenie tych danych pozwala określić, czy wzrost wykorzystania pamięci jest spowodowany przez zachowane odwołania do Javy czy przez alokacje tylko natywne.Prywatna inna i sterta natywna: w
dumpsys meminfoalokacje natywne C/C++ i niestandardowe mapowania pamięci (takie jakPartitionAllocChromium lub wbudowane sterty środowiska wykonawczego JavaScriptu) pojawiają się w sekcji Sterta natywna i Prywatna inna, a nie Sterta Java.
Więcej informacji o licznikach pamięci procesów i ich kategoriach znajdziesz w słowniczku pamięci procesów.
Praktyczne przepływy pracy związane z diagnostyką
WebView działa w wielu procesach i przydziela pamięć natywną, dlatego do sprawdzania jej wykorzystania używaj tych narzędzi i technik:
Narzędzia do profilowania i diagnostyki
Aby sprawdzić alokacje pamięci i zdiagnozować wycieki, użyj tych narzędzi:
Memory Profiler w Android Studio: używaj narzędzia Memory Profiler do wizualizacji alokacji natywnych, śledzenia kategorii pamięci w czasie i wykrywania wycieków
Activitypodczas przechodzenia między ekranami.Śledzenie pamięci za pomocą Perfetto: używaj Perfetto do rejestrowania liczników pamięci na poziomie systemu (takich jak RSS i anonimowy RSS), aby obserwować ogólny wzrost wykorzystania pamięci. Pamiętaj, że alokacje silnika natywnego
WebViewnie generują stosów wywołań w narzędziu do profilowania sterty Perfetto. Za pomocą Narzędzi deweloperskich w Chrome możesz sprawdzać zrzuty sterty JavaScriptu i alokacje DOM w treściach z internetu.
Sprawdzanie liczby aktywnych obiektów
Aby określić, czy wzrost wykorzystania pamięci jest spowodowany przez zachowane obiekty platformy Java (takie jak komponenty interfejsu) czy przez alokacje natywne, sprawdź sekcję Objects w dumpsys meminfo:
adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"Dane wyjściowe zawierają liczbę aktywnych obiektów:
Objects
Views: 142 ViewRootImpl: 1
AppContexts: 3 Activities: 1
Assets: 12 AssetManagers: 0
Local Binders: 32 Proxy Binders: 45
Parcel memory: 15 Parcel count: 30
Death Recipients: 2 WebViews: 1
W tej sekcji wyświetlana jest liczba aktywnych obiektów platformy, uchwytów IPC i przydziałów Parcel. W przypadku diagnostyki WebView skup się przede wszystkim na Activities i WebViews.
Wielokrotnie wykonaj docelową interakcję użytkownika (np. otwarcie i zamknięcie ekranu internetowego) i porównaj liczby:
Wyciek instancji: jeśli wartość
WebViewslubActivitieszwiększa się przy każdej nawigacji i nie wraca do poziomu odniesienia, w aplikacji występuje wyciek instancji JavaWebViewlub hostaActivity(np. z powodu brakuViewGroup.removeView()lub zachowanych odwołań do detektora). Ponieważ wyciekający obiektActivityprzypina całe drzewo widoku i zdekodowane zasoby obrazu w pamięci, wielokrotne wizyty szybko wyczerpią stertę Javy i spowodują awarieOutOfMemoryError.Wyciek natywny lub DOM: jeśli wartości
WebViewsiActivitiespozostają stałe, a całkowity rozmiar zbioru roboczego procesu i prywatne inne nadal rosną, wyciek pochodzi z niezwolnionych zasobów natywnych, elementów DOM lub powiązań silnika JavaScriptu. Te alokacje znajdują się w pamięci natywnej i pomijają moduł odzyskiwania pamięci ART, dlatego są niewidoczne dla standardowych narzędzi do wykrywania wycieków w języku Java i gromadzą się, dopóki system operacyjny nie zakończy działania aplikacji.
Profilowanie odizolowanego procesu renderowania za pomocą interfejsu wiersza poleceń
Uruchomienie polecenia dumpsys meminfo z nazwą pakietu aplikacji powoduje wyświetlenie informacji o pamięci tylko dla głównego procesu hosta. Aby sprawdzić odizolowany proces mechanizmu renderowania, w którym renderowane są strony internetowe:
Znajdź identyfikator procesu (PID) usługi izolowanego renderowania:
adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"Dane wyjściowe zawierają rekord wyizolowanego procesu i jego identyfikator PID RENDERER_PID (np.
22155):Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}Sprawdź zestawienie pamięci procesu renderowania, używając jego identyfikatora PID:
adb shell dumpsys meminfo <var>RENDERER_PID</var>Sprawdź proces aplikacji hosta, aby ocenić wykorzystanie zasobów po stronie przeglądarki:
adb shell dumpsys meminfo <var>PACKAGE_NAME</var>
Sprawdzanie map i alokacji pamięci
Aby sprawdzić, które podsystemy natywne lub alokatory zajmują pamięć anonimową, sprawdź mapy pamięci procesu:
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"W tabeli poniżej znajdziesz listę typowych tagów pamięci anonimowej i ich znaczenie dla wzrostu wykorzystania pamięci:
| Tag pamięci | Subsystem | trafność względem treści w aplikacji i treści z internetu, | Częsta przyczyna zwiększenia ilości pamięci? |
|---|---|---|---|
[anon:partition_alloc] |
Chromium PartitionAlloc | Alokacje drzew DOM, buforów renderowania, sterty JavaScriptu V8 i wykonywania WebAssembly w WebView. |
Tak (wysoki): wczytywanie ciężkich stron internetowych, obiektów DOM z dużą ilością multimediów lub niepowodzenie wywołania destroy() w przypadku odrzuconych instancji WebView bezpośrednio zwiększa wartość tego tagu. |
[anon:scudo...] lub [anon:libc_malloc] |
Alokatory natywnej sterty Androida (Scudo / jemalloc) | Ogólne alokacje natywne C/C++ używane przez biblioteki NDK, mostki JNI i natywne potoki graficzne. | Tak (umiarkowany lub wysoki): wzrost występuje, gdy natywne kodery JNI lub zależności C++ innych firm zachowują niezwolnione przydziały podczas nawigacji. |
[anon:...] (na przykład [anon:quickjs_heap...]) |
Niestandardowe skrypty lub natywne środowiska wykonawcze | wbudowane silniki JavaScriptu, niestandardowe środowiska wykonawcze WebAssembly lub niestandardowe pule buforów natywnych; | Tak (w zależności od kontekstu): często występuje w aplikacjach hybrydowych, które uruchamiają mechanizmy skryptowe obok widoków natywnych i nie czyszczą powiązań środowiska wykonawczego. |
Ograniczenia interfejsów API pamięci w aplikacji
Interfejsy API pamięci w aplikacji (takie jak Debug.getMemoryInfo lub ActivityManager.getProcessMemoryInfo) mierzą tylko proces wywołujący.
W trybie wieloprocesowym te interfejsy API nie mogą rejestrować pamięci wykorzystywanej przez odizolowany proces renderowania. Aby uzyskać dokładną ocenę łącznej ilości pamięci, użyj narzędzi systemowych, takich jak dumpsys meminfo, Perfetto lub Profiler w Android Studio.
Klasyfikowanie wysokiego wykorzystania pamięci w aplikacji hybrydowej
Podczas diagnozowania niewyjaśnionego wzrostu wykorzystania pamięci podczas cyklicznych interakcji z WebView (takich jak otwieranie linków internetowych lub przeglądanie kanałów internetowych) użyj tego przepływu pracy, aby ustalić, czy wyciek pochodzi z warstwy Javy czy z silnika natywnego:
Określ typ wycieku (Java lub natywny): uruchom
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"przed powtarzającymi się przejściami użytkownika (np. otwieraniem i zamykaniem artykułów w internecie lub przeglądaniem kanałów) i po nich.- Obserwacja: jeśli liczba obiektów
ActivitiesiWebViewspozostaje stabilna (np. 1–2 aktywne instancje), aplikacja nie powoduje wycieku kontekstówActivityani instancjiWebViewJavy.
- Obserwacja: jeśli liczba obiektów
Pomiar zmiany wykorzystania pamięci między interakcjami (śledzenie szeregów czasowych): wykonaj zrzuty
dumpsys meminfopodczas wielu interakcji użytkownika, aby obliczyć współczynnik alokacji na przejście:- Obserwacja: sterta Javy pozostaje ograniczona i działa prawidłowo (skoki podczas używania i spadki po czyszczeniu pamięci), ale Private Other i Native Heap stale rosną o kilka megabajtów na przejście. To dowodzi, że wyciek występuje całkowicie w pamięci natywnej poza środowiskiem wykonawczym ART.
Standardowe zrzuty sterty Java (
.hprof) nie będą wykazywać żadnych problemów.
- Obserwacja: sterta Javy pozostaje ograniczona i działa prawidłowo (skoki podczas używania i spadki po czyszczeniu pamięci), ale Private Other i Native Heap stale rosną o kilka megabajtów na przejście. To dowodzi, że wyciek występuje całkowicie w pamięci natywnej poza środowiskiem wykonawczym ART.
Standardowe zrzuty sterty Java (
Sprawdź anonimowe mapy pamięci: sprawdź mapy pamięci procesów za pomocą ADB (patrz Sprawdzanie map pamięci i alokacji):
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"- Obserwacja: wzrost wykorzystania pamięci koncentruje się w stertach
[anon:partition_alloc]lub wbudowanego mechanizmu skryptowego, czemu towarzyszy powolny wzrost liczby globalnych odwołań JNI. Oznacza to, że chociaż widoki Java zostały zastąpione, bazowe obiekty strony natywnej lub powiązania JavaScriptu nie zostały zwolnione.
- Obserwacja: wzrost wykorzystania pamięci koncentruje się w stertach
Rozwiązanie:
- Upewnij się, że każdy poddany recyklingowi lub odrzucony element
WebViewwyraźnie zatrzymuje aktywne skrypty (stopLoading()), czyści historię i wywołujedestroy(). - Usuń niestandardowe wywołania zwrotne mostu JavaScript lub globalne odwołania JNI powiązane z zamkniętymi widokami.
- Sprawdź, czy po przejściach nawigacyjnych
Private Otheri proces RSS stabilizują się.
- Upewnij się, że każdy poddany recyklingowi lub odrzucony element
Dodatkowe materiały
Więcej informacji o debugowaniu i profilowaniu pamięci oraz wydajności WebView znajdziesz w tych materiałach: