Zarządzanie pamięcią WebView i diagnozowanie jej

WebView uruchamia kod natywny w wielu procesach, aby renderować treści internetowe w aplikacji na Androida. Pozostawienie instancji WebView bez zarządzania może prowadzić do wycieków pamięci, awarii z powodu braku pamięci (OOM) i pogorszenia wydajności aplikacji.

Z tego dokumentu dowiesz się, jak działa model pamięci WebView w przypadku wielu procesów, jak prawidłowo zarządzać cyklem życia, aby zapobiegać wyciekom, oraz jak diagnozować problemy z pamięcią.

Architektura pamięci WebView

Aby skutecznie zarządzać pamięcią WebView, musisz wiedzieć, jak Android przydziela zasoby na potrzeby treści internetowych:

  • Wykonywanie w wielu procesach: w Androidzie 8.0 (poziom API 26) i nowszych wersjach WebView oddziela treści internetowe od podstawowych funkcji aplikacji w wielu procesach (na urządzeniach z małą ilością pamięci RAM może wrócić do jednego procesu):

    • Proces hosta (przeglądarki): główny proces aplikacji, w którym działa kod Activity oraz kod Java lub Kotlin.
    • Izolowany proces renderujący: oddzielny proces w piaskownicy (SandboxedProcessService), który analizuje kod HTML i CSS, wykonuje kod JavaScript i renderuje strony internetowe.
  • Wykorzystanie pamięci natywnej: większość WebView pamięci, w tym renderowana grafika, drzewo DOM i pamięć środowiska wykonawczego JavaScript, jest przydzielana w pamięci natywnej, a nie na stercie Java. Zrzut sterty Java (.hprof) pokazuje tylko lekki obiekt otoki Java i nie rejestruje rzeczywistej ilości pamięci używanej przez treści internetowe.

  • Wpływ pamięci natywnej na system: w przeciwieństwie do alokacji na stercie Java, które są ograniczone limitem maxHeap aplikacji i szybko kończą się błędem OutOfMemoryError, pamięć natywna może cicho rosnąć do gigabajtów. Gdy niezwolniona pamięć natywna wypełnia pamięć RAM i przestrzeń wymiany (zRAM), mechanizm Low Memory Killer (LMK) w Androidzie zaczyna zamykać procesy działające w tle, aby odzyskać pamięć. Powoduje to pogorszenie ogólnej wielozadaniowości urządzenia, a w końcu zamknięcie aplikacji działającej na pierwszym planie.

Zarządzanie cyklem życia WebView

Prawidłowe zarządzanie cyklem życia jest niezbędne do zapobiegania wyciekom pamięci. Częstym błędem jest założenie, że usunięcie WebView z układu lub automatyczne zakończenie działania Activity zwalnia jego pamięć.

Aby zapewnić całkowite wyczyszczenie zarówno odwołań do kontekstu Java, jak i natywnych zasobów renderowania, musisz wyraźnie zorganizować sekwencję zamykania w cyklu życia komponentu hosta (np. onDestroy()), zatrzymując wykonywanie aktywnej strony, odłączając widok od kontenera i zwalniając natywne powiązania.

Zwalnianie miejsca w instancjach WebView

Aby zapewnić prawidłowe zamknięcie i zwolnienie zasobów po zniszczeniu Activity lub Fragment, wykonaj te czynności:

  1. Usuń WebView z kontenera nadrzędnego (ViewGroup).
  2. Zatrzymaj aktywne wczytywanie i wyczyść historię nawigacji.
  3. Wywołaj metodę destroy().
  4. Wyczyść odwołanie do null.

Poniższy przykład pokazuje, jak prawidłowo zwolnić miejsce w 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 pracę w tle w internecie. Możesz jednak zauważyć, że pamięć fizyczna procesu (rozmiar zbioru roboczego) nie spadnie od razu do poziomu sprzed użycia `WebView`.

To normalne zjawisko. Pamięć podręczna środowiska wykonawczego, biblioteki współdzielone i przydzielone strony pamięci pozostają w procesie, dopóki system operacyjny ich nie odzyska lub proces się nie zakończy. Głównym celem metody destroy() jest zapobieganie kumulatywnym wyciekom pamięci Activity, gdy użytkownicy przechodzą między ekranami opartymi na internecie.

Kluczowe dane debugowania

Podczas analizowania zużycia pamięci przez WebView skup się na tych danych:

  • Rozmiar zbioru roboczego (RSS): łączna ilość pamięci RAM przypisana do procesu, w tym kod i biblioteki współdzielone (w Profilerze Android Studio oznaczona jako Łącznie).

  • Anonimowy rozmiar zbioru roboczego (RssAnon): pamięć przydzielona bezpośrednio przez proces, która nie jest przechowywana w pliku na dysku (np. sterta natywna i alokacje środowiska wykonawczego JavaScript). Reprezentuje ona główny koszt pamięci treści internetowych (w Profilerze Android Studio oznaczona jako Przydzielona).

  • Zajętość pamięci prywatnej (PMF): suma anonimowego rozmiaru zbioru roboczego i przestrzeni wymiany (zRAM). PMF odzwierciedla rzeczywiste obciążenie pamięci, którego nie można usunąć, a które aplikacja nakłada na system.

  • PMF przeglądarki a PMF renderera: pamięć używana przez główny proces aplikacji a pamięć używana przez izolowany proces renderujący. Duże treści internetowe powodują skoki zużycia pamięci głównie w procesie renderującym.

  • 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 zużycia pamięci jest spowodowany zachowanymi odwołaniami Java, czy alokacjami tylko natywnymi.

  • Pamięć prywatna i sterta natywna: w dumpsys meminfo alokacje natywne C/C++ i niestandardowe mapowania pamięci (np. PartitionAlloc Chromium lub sterty osadzonego środowiska wykonawczego JavaScript) pojawiają się w sekcji Native Heap i Private Other, a nie w Java Heap.

Więcej informacji o licznikach pamięci procesu i ich kategoriach znajdziesz w glosariuszu pamięci procesu.

Praktyczne procedury diagnostyczne

Ponieważ WebView działa w wielu procesach i przydziela pamięć natywną, do sprawdzenia jego zajętości użyj tych narzędzi i technik:

Narzędzia do profilowania i diagnostyki

Aby sprawdzić alokacje pamięci i zdiagnozować wycieki, użyj tych narzędzi:

  • Profiler pamięci w Android Studio: używaj Profilera pamięci do wizualizowania alokacji natywnych, śledzenia kategorii pamięci w czasie i wykrywania Activity podczas przejść między ekranami.

  • Śledzenie pamięci za pomocą Perfetto: używaj Perfetto do rejestrowania liczników pamięci na poziomie systemu (np. RSS i anonimowy RSS), aby obserwować ogólny wzrost zużycia pamięci . Pamiętaj, że WebView alokacje natywnego silnika nie generują stosów wywołań w narzędziu do profilowania sterty Perfetto. Używaj Narzędzi deweloperskich w Chrome, aby sprawdzać zrzuty sterty JavaScript i alokacje DOM w treściach internetowych.

Sprawdzanie liczby aktywnych obiektów

Aby określić, czy wzrost zużycia pamięci jest spowodowany zachowanymi obiektami platformy Java (np. komponentami interfejsu) czy alokacjami natywnymi, sprawdź sekcję Objects w dumpsys meminfo:

adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"

Dane wyjściowe pokazują 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

Ta sekcja zawiera liczbę aktywnych obiektów frameworka, uchwytów IPC i alokacji Parcel. W przypadku diagnostyki WebView skup się przede wszystkim na Activities i WebViews.

Powtarzaj interakcję użytkownika (np. otwieranie i zamykanie ekranu internetowego) i porównuj liczby:

  • Wyciek instancji: jeśli liczba WebViews lub Activities zwiększa się przy każdej nawigacji i nie wraca do wartości bazowej, aplikacja powoduje wyciek instancji WebView Java lub Activity hosta (np. z powodu braku ViewGroup.removeView() lub zachowanych odwołań do odbiornika). Ponieważ wyciek Activity przypina całe drzewo widoków i zdekodowane zasoby obrazów w pamięci, powtarzające się wizyty szybko wyczerpują stertę Java i powodują awarie OutOfMemoryError.

  • Wyciek natywny lub DOM: jeśli WebViews i Activities pozostają stałe, a łączny rozmiar zbioru roboczego procesu i Private Other nadal rosną, wyciek pochodzi z niezwolnionych zasobów natywnych, elementów DOM lub powiązań silnika JavaScript. Ponieważ te alokacje znajdują się w pamięci natywnej i omijają moduł zbierania śmieci ART, pozostają niewidoczne dla standardowych narzędzi do wykrywania wycieków Java i nadal się gromadzą, dopóki system operacyjny nie zakończy działania aplikacji.

Profilowanie izolowanego procesu renderującego za pomocą interfejsu wiersza poleceń

Uruchomienie dumpsys meminfo z nazwą pakietu aplikacji powoduje wyświetlenie tylko pamięci głównego procesu hosta. Aby sprawdzić izolowany proces renderujący, w którym renderowane są strony internetowe:

  1. Znajdź identyfikator procesu (PID) izolowanej usługi renderującej:

    adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"

    Dane wyjściowe pokazują rekord izolowanego procesu i jego PID RENDERER_PID (np. 22155):

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Sprawdź podział pamięci procesu renderującego za pomocą jego PID:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. Sprawdź proces aplikacji hosta, aby ocenić zajętość po stronie przeglądarki:

    adb shell dumpsys meminfo <var>PACKAGE_NAME</var>

Sprawdzanie map pamięci i alokacji

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 zużycia pamięci:

Tag pamięci Podsystem Znaczenie dla aplikacji i treści internetowych Częsta przyczyna wzrostu zużycia pamięci?
[anon:partition_alloc] Chromium PartitionAlloc Alokacje dla drzew DOM, buforów renderowania, sterty JavaScript V8 i wykonywania WebAssembly w WebView. Tak (wysokie): wczytywanie dużych stron internetowych, bogatych w multimedia drzew DOM lub brak wywołania metody destroy() w przypadku odrzuconych instancji WebView bezpośrednio zwiększa wartość tego tagu.
[anon:scudo...] lub [anon:libc_malloc] Alokatory sterty natywnej Androida (Scudo / jemalloc) Ogólne alokacje natywne C/C++ używane przez biblioteki NDK, mostki JNI i natywne potoki graficzne. Tak (średnie do wysokiego): wzrost występuje, gdy natywne otoki JNI lub zależności C++ innych firm zachowują niezwolnione alokacje podczas nawigacji.
[anon:...] (np. [anon:quickjs_heap...]) Skrypty niestandardowe lub natywne środowiska wykonawcze Osadzone silniki JavaScript, niestandardowe środowiska wykonawcze WebAssembly lub niestandardowe pule buforów natywnych. Tak (zależne od kontekstu): często spotykane w aplikacjach hybrydowych, które wykonują silniki skryptów 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 (np. Debug.getMemoryInfo lub ActivityManager.getProcessMemoryInfo) mierzą tylko proces wywołujący. W trybie wieloprocesowym te interfejsy API nie mogą rejestrować pamięci zużywanej przez izolowany proces renderujący. Aby uzyskać dokładną ocenę całkowitej ilości pamięci, używaj narzędzi systemowych, takich jak dumpsys meminfo, Perfetto lub Profiler Android Studio.

Triage dużej ilości pamięci w aplikacji hybrydowej

Podczas diagnozowania niewyjaśnionego wzrostu zużycia pamięci podczas powtarzających się interakcji z WebView (np. otwierania linków internetowych lub przeglądania kanałów opartych na internecie) użyj tego procesu triage, aby sprawdzić, czy wyciek pochodzi z warstwy Java, czy z silnika natywnego:

  1. Izolowanie typu wycieku (Java a natywny): Uruchom dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" przed i po powtarzających się przejściach użytkownika (np. otwieraniu i zamykaniu artykułów internetowych lub przesuwaniu palcem po kanałach).

    • Obserwacja: jeśli liczba Activities i WebViews pozostaje stała (np. 1–2 aktywne instancje), aplikacja nie powoduje wycieku kontekstów Activity ani instancji WebView Java.
  2. Mierzenie delty pamięci podczas interakcji (śledzenie szeregów czasowych): rejestruj zrzuty dumpsys meminfo podczas wielu interakcji użytkownika, aby obliczyć współczynnik alokacji na przejście:

    • Obserwacja: sterta Java pozostaje ograniczona i w dobrym stanie (wzrasta podczas używania i spada po zebraniu śmieci), ale Private Other i Native Heap stale rosną o kilka megabajtów na przejście. Dowodzi to, że wyciek występuje w całości w pamięci natywnej poza środowiskiem wykonawczym ART. Standardowe zrzuty sterty Java (.hprof) nie wykażą żadnych problemów.
  3. Sprawdzanie map pamięci anonimowej: sprawdź mapy pamięci procesu za pomocą ADB (patrz Sprawdzanie map pamięci i alokacji):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Obserwacja: wzrost zużycia pamięci koncentruje się w [anon:partition_alloc] lub osadzonych stertach silnika skryptów, czemu towarzyszy powolny wzrost globalnych odwołań JNI. Wskazuje to, że chociaż widoki Java zostały zastąpione, podstawowe natywne obiekty strony lub powiązania JavaScript nie zostały zwolnione.
  4. Działania naprawcze:

    • Upewnij się, że każdy odzyskany lub odrzucony WebView wyraźnie zatrzymuje aktywne skrypty (stopLoading()), czyści historię i wywołuje metodę destroy().
    • Zamykaj niestandardowe wywołania zwrotne mostka JavaScript lub globalne odwołania JNI powiązane z odrzuconymi widokami.
    • Sprawdź, czy po przejściach nawigacyjnych stabilizują się wartości Private Other i rozmiaru zbioru roboczego procesu.

Dodatkowe materiały

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