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 niezarządzanych instancji WebView może prowadzić do wycieków pamięci, awarii z powodu braku pamięci i pogorszenia wydajności aplikacji.

W tym dokumencie wyjaśniamy WebViewmodel pamięci wieloprocesowej, opisujemy, jak prawidłowo zarządzać jego cyklem życia, aby zapobiegać wyciekom, i przedstawiamy praktyczne przepływy pracy do diagnozowania problemów z pamięcią.

Poznaj architekturę pamięci WebView

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

  • Wieloprocesowe wykonywanie: w Androidzie 8.0 (poziom interfejsu API 26) i nowszymWebView oddziela treści internetowe od podstawowych funkcji aplikacji w ramach wielu procesów (na urządzeniach z małą ilością pamięci RAM może powrócić do jednego procesu):

    • Proces hosta (przeglądarki): główny proces aplikacji, w którym działa kod Activity oraz kod Java lub Kotlin.
    • Proces renderowania w izolacji: 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ść pamięci WebView, w tym renderowana grafika, drzewo DOM i pamięć środowiska wykonawczego JavaScriptu, jest przydzielana w pamięci natywnej, a nie na stercie Javy. Zrzut sterty Javy (.hprof) pokazuje tylko lekki obiekt opakowujący 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 sterty Javy, które są ograniczone limitem maxHeap aplikacji i szybko kończą się niepowodzeniem z 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ęć. Obniża to ogólną wydajność wielozadaniowości urządzenia, a w końcu zamyka 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 założenie, że usunięcie elementu WebView z układu lub automatyczne zakończenie działania elementu Activity zwalnia jego pamięć.

Aby zapewnić całkowite wyczyszczenie zarówno odwołań do kontekstu Java, jak i zasobów renderowania natywnego, 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 powiązania natywne.

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ń element WebView z kontenera nadrzędnego (ViewGroup).
  2. Zatrzymaj aktywne ładowanie i wyczyść historię nawigacji.
  3. Zadzwoń: destroy()
  4. Usuń odniesienie 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();
}

Informacje o pamięci po zniszczeniu

Gdy zadzwonisz pod numer destroy(), system zwalnia kontekst Activity, czyści hierarchie widoków i zatrzymuje 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 użycia WebView.

Jest to normalne zachowanie. 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 destroy() jest zapobieganie Activity wyciekom pamięci, gdy użytkownicy przechodzą między ekranami opartymi na internecie.

Kluczowe dane do debugowania

Analizując WebView zużycie pamięci, skup się na tych danych:

  • Rozmiar zestawu rezydentnego (RSS): całkowita ilość pamięci RAM przypisana do procesu, w tym współdzielony kod i biblioteki (w profilerze Androida Studio oznaczona jako Łącznie).

  • Anonimowy RSS (RssAnon): pamięć przydzielona bezpośrednio przez proces, która nie jest powiązana z plikiem na dysku (np. natywna sterta i alokacje środowiska wykonawczego JavaScript). Jest to główny koszt pamięci treści internetowych (w profilerze Android Studio oznaczony jako Przydzielono).

  • Wykorzystanie pamięci prywatnej (PMF): suma anonimowej pamięci RSS i przestrzeni wymiany (zRAM). PMF odzwierciedla rzeczywiste, nieusuwalne obciążenie pamięci, jakie aplikacja wywiera na system.

  • PMF przeglądarki a PMF renderowania: pamięć używana przez główny proces aplikacji a pamięć używana przez odizolowany proces renderowania. Duże ilości treści internetowych powodują wzrosty głównie w procesie renderowania.

  • Liczba aktywnych obiektów (WebViews, Activities, Views): liczba aktywnych instancji interfejsu, kontekstu i WebView przechowywanych w pamięci. Śledzenie tych informacji pozwala określić, czy wzrost pamięci jest spowodowany zachowanymi odwołaniami do Javy czy alokacjami tylko w kodzie natywnym.

  • Prywatna pamięć inna niż sterta i sterta natywna:dumpsys meminfo alokacje natywne w C/C++ i niestandardowe mapowania pamięci (np. sterty Chromium PartitionAlloc lub osadzone sterty środowiska wykonawczego JavaScript) pojawiają się w sekcjach „Sterta natywna” i „Prywatna pamięć inna niż sterta”, a nie w sekcji „Sterta Javy”.

Więcej informacji o licznikach pamięci procesu i ich kategoriach znajdziesz w słowniczku pamięci procesu.

Praktyczne przepływy pracy diagnostycznej

WebView działa w wielu procesach i przydziela pamięć natywną, więc do sprawdzenia jego śladu użyj tych narzędzi i technik:

Narzędzia do profilowania i diagnostyki

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

  • Narzędzie Memory Profiler w Android Studio: używaj Memory Profilera, aby wizualizować alokacje natywne, śledzić kategorie pamięci w czasie i wykrywać Activity wycieki podczas przejść 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 WebViewalokacje natywnego silnika nie generują stosów wywołań w narzędziu do profilowania sterty Perfetto. Użyj Narzędzi deweloperskich w Chrome, aby sprawdzić zrzuty sterty JavaScript i alokacje DOM w treściach internetowych.

Sprawdzanie liczby aktywnych obiektów

Aby sprawdzić, czy wzrost pamięci jest spowodowany przez zachowane obiekty platformy Java (np. komponenty interfejsu) czy przez alokacje natywne, sprawdź sekcję Objectsdumpsys 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świetlane są liczby aktywnych obiektów platformy, uchwytów IPC i przydziałów Parcel. W przypadku diagnostyki WebView skup się przede wszystkim na ActivitiesWebViews.

Wielokrotnie wykonaj interakcję użytkownika docelowego (np. otwórz i zamknij ekran internetowy) i porównaj liczby:

  • Wyciek instancji: jeśli wartości WebViews lub Activities rosną przy każdej nawigacji i nie wracają do wartości bazowej, oznacza to, że w aplikacji występuje wyciek instancji Java WebView lub hosta Activity (np. z powodu braku ViewGroup.removeView() lub zachowanych odwołań do słuchacza). Wyciek pamięciActivity blokuje cały drzewo widoków i zdekodowane zasoby obrazów w pamięci, więc wielokrotne odwiedzanie strony szybko wyczerpuje stertę Javy i powodujeOutOfMemoryError awarie.

  • Wyciek natywny lub wyciek DOM: jeśli wartości WebViewsActivities pozostają stałe, a całkowity rozmiar pamięci RAM procesu i inne prywatne nadal rosną, wyciek pochodzi z niezwolnionych zasobów natywnych, elementów DOM lub powiązań silnika JavaScript. Ponieważ te przydziały znajdują się w pamięci natywnej i są pomijane przez moduł odśmiecania ART, pozostają niewidoczne dla standardowych narzędzi do wykrywania wycieków pamięci w Javie i nadal się gromadzą, dopóki system operacyjny nie zakończy działania aplikacji.

Profilowanie izolowanego procesu renderowania za pomocą interfejsu wiersza poleceń

Uruchomienie polecenia dumpsys meminfo z nazwą pakietu aplikacji spowoduje wyświetlenie tylko pamięci głównego procesu hosta. Aby sprawdzić odizolowany proces 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"

    W danych wyjściowych pojawi się rekord izolowanego procesu i jego identyfikator PIDRENDERER_PID (np. 22155):

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Sprawdź podział 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ć rozmiar po stronie przeglądarki:

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

Sprawdzanie map pamięci i przydziałów

Aby sprawdzić, które natywne podsystemy 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 pamięci:

Tag pamięci Podsystem Trafność względem treści aplikacji i witryny Częsta przyczyna zwiększenia pamięci?
[anon:partition_alloc] Chromium PartitionAlloc Przydziały dla drzew DOM, buforów renderowania, sterty JavaScript V8 i wykonywania WebAssembly w WebView. Tak (wysoki): wczytywanie dużych stron internetowych, DOM-ów z dużą ilością multimediów lub brak wywołania funkcji 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 natywne alokacje C/C++ używane przez biblioteki NDK, mosty JNI i natywne potoki graficzne. Tak (średni lub wysoki): wzrost następuje, gdy natywne otoki JNI lub zależności C++ innych firm zachowują niezwolnione przydziały w różnych nawigacjach.
[anon:...] (np. [anon:quickjs_heap...]) Skrypty niestandardowe lub natywne środowiska wykonawcze wbudowane silniki JavaScript, niestandardowe środowiska wykonawcze WebAssembly lub niestandardowe natywne pule buforów. Tak (zależne od kontekstu): często występuje 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ływania. W trybie wieloprocesowym te interfejsy API nie mogą rejestrować pamięci zużywanej przez proces renderowania w izolacji. Aby uzyskać dokładną ocenę łącznej pamięci, korzystaj z narzędzi systemowych, takich jak dumpsys meminfo, Perfetto czy Profiler w Android Studio.

Triageowanie wysokiego wykorzystania pamięci w aplikacji hybrydowej

Podczas diagnozowania niewyjaśnionego wzrostu pamięci podczas powtarzających się interakcjiWebView (takich jak otwieranie linków internetowych lub przeglądanie kart internetowych) użyj tego przepływu pracy, aby sprawdzić, czy wyciek pochodzi z warstwy Java czy z silnika natywnego:

  1. Określ typ wycieku (Java czy natywny): uruchom dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" przed i po wielokrotnym przełączaniu się użytkownika (np. otwieraniu i zamykaniu artykułów w internecie lub przesuwaniu palcem po treściach).

    • Obserwacja: jeśli liczby ActivitiesWebViews pozostają na stałym poziomie (np. 1–2 aktywne instancje), aplikacja nie powoduje wycieku kontekstów Activity ani instancji WebView w języku Java.
  2. Pomiar różnicy w pamięci w różnych interakcjach (śledzenie szeregów czasowych): wykonuj dumpsys meminfo migawek w różnych interakcjach użytkownika, aby obliczyć współczynnik alokacji na przejście:

    • Obserwacja: sterta Javy pozostaje ograniczona i w dobrej kondycji (wzrasta podczas użytkowania i spada po odśmiecaniu), ale Pamięć prywatna – inneStos natywny stale rosną o kilka megabajtów na przejście. To dowodzi, że wyciek pamięci występuje w pamięci natywnej poza środowiskiem wykonawczym ART. Standardowe zrzuty sterty Javy (.hprof) nie wykażą żadnych problemów.
  3. Sprawdzanie anonimowych map pamięci: sprawdź mapy pamięci procesu za pomocą ADB (patrz Sprawdzanie map pamięci i przydziałów):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Obserwacja: wzrost pamięci koncentruje się w stertach silnika skryptowego[anon:partition_alloc] lub w stertach silnika skryptowego osadzonego, czemu towarzyszy powolny wzrost globalnych odwołań JNI. Oznacza to, że widoki Java zostały zastąpione, ale bazowe 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 destroy().
    • Usuwaj wywołania zwrotne niestandardowego mostka JavaScript lub globalne odwołania JNI powiązane z zamkniętymi widokami.
    • Sprawdź, czy Private Other i przetwarzanie RSS stabilizują się po przejściach nawigacyjnych.

Dodatkowe materiały

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