Zarządzanie pamięcią WebView i diagnozowanie jej

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 WebView oddziela 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 Activity oraz 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.
  • 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 maxHeap aplikacji i szybko kończą się błędem OutOfMemoryError, 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:

  1. Usuń element WebView z kontenera nadrzędnego (ViewGroup).
  2. Zatrzymaj aktywne wczytywanie i wyczyść historię nawigacji.
  3. Zadzwoń: destroy()
  4. 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 i WebView przechowywanych 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 meminfo alokacje natywne C/C++ i niestandardowe mapowania pamięci (takie jak PartitionAlloc Chromium 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 Activity podczas 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 WebView nie 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ść WebViews lub Activities zwiększa się przy każdej nawigacji i nie wraca do poziomu odniesienia, w aplikacji występuje wyciek instancji Java WebView lub hosta Activity (np. z powodu braku ViewGroup.removeView() lub zachowanych odwołań do detektora). Ponieważ wyciekający obiekt Activity przypina całe drzewo widoku i zdekodowane zasoby obrazu w pamięci, wielokrotne wizyty szybko wyczerpią stertę Javy i spowodują awarie OutOfMemoryError.

  • Wyciek natywny lub DOM: jeśli wartości WebViews i Activities pozostają 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:

  1. 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:...}
    
  2. Sprawdź zestawienie pamięci procesu renderowania, używając jego identyfikatora PID:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. 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:

  1. 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 Activities i WebViews pozostaje stabilna (np. 1–2 aktywne instancje), aplikacja nie powoduje wycieku kontekstów Activity ani instancji WebView Javy.
  2. Pomiar zmiany wykorzystania pamięci między interakcjami (śledzenie szeregów czasowych): wykonaj zrzuty dumpsys meminfo podczas 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.
  3. 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.
  4. Rozwiązanie:

    • Upewnij się, że każdy poddany recyklingowi lub odrzucony element WebView wyraźnie zatrzymuje aktywne skrypty (stopLoading()), czyści historię i wywołuje destroy().
    • 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 Other i proces RSS stabilizują się.

Dodatkowe materiały

Więcej informacji o debugowaniu i profilowaniu pamięci oraz wydajności WebView znajdziesz w tych materiałach: