Zarządzanie pamięcią WebView i diagnozowanie jej

WebView uruchamia kod natywny w wielu procesach, aby renderować treści internetowe w Twojej 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 wieloprocesowej WebView, 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.
  • Zajętość pamięci natywnej: większość pamięci WebView, 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 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 jego kontenera i zwalniając natywne powiązania.

Czyszczenie instancji WebView

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

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

Ten przykład pokazuje, jak prawidłowo wyczyścić 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 `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ść fizycznej pamięci RAM mapowanej na proces, w tym kod i biblioteki współdzielone (w Profilerze Android Studio oznaczona jako Łącznie).

  • Anonimowy RSS (RssAnon): pamięć przydzielona bezpośrednio przez proces, która nie jest wspierana przez plik 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 RSS i przestrzeni wymiany (zRAM). PMF odzwierciedla rzeczywiste, nieusuwalne obciążenie pamięci, jakie aplikacja nakłada na system.

  • PMF przeglądarki a PMF renderowania: pamięć używana przez główny proces aplikacji a pamięć używana przez izolowany proces renderujący. Duże treści internetowe powodują skoki przede wszystkim 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 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 w 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 przepływy pracy diagnostycznej

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:

Sprawdzanie liczby aktywnych obiektów

Aby określić, czy wzrost pamięci jest spowodowany zachowanymi obiektami frameworka 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 wyświetlają 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 wyświetla 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 ma wyciek instancji WebView w Javie 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 RSS 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ł odśmiecania ART, pozostają niewidoczne dla standardowych narzędzi do wykrywania wycieków w Javie i nadal się kumulują, dopóki system operacyjny nie zakończy 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 renderowania:

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

    Dane wyjściowe wyświetlają 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 lub alokatory natywne 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 pamięci:

Tag pamięci Podsystem Znaczenie dla aplikacji i treści internetowych Częsta przyczyna wzrostu 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 DOM lub brak wywołania destroy() w przypadku odrzuconych instancji WebView bezpośrednio zwiększa ten tag.
[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 (umiarkowane 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 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 (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 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 przepływu pracy 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 ma wycieku kontekstów Activity ani instancji WebView w Javie.
  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 odśmiecaniu), 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 pamięci koncentruje się w [anon:partition_alloc] lub osadzonych stertach silnika skryptowego, czemu towarzyszy powolny wzrost globalnych odwołań JNI. Wskazuje to, że chociaż widoki Java zostały zastąpione, bazowe obiekty strony natywnej 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 RSS procesu.

Dodatkowe materiały

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