WebView i pamięć

WebView to zaawansowany komponent, który umożliwia wyświetlanie treści internetowych w aplikacji na Androida. Jest to jednak w zasadzie pełnoprawny silnik przeglądarki (Chromium), dlatego ma znaczące wykorzystanie pamięci i złożoną architekturę wieloprocesową.

Informacje techniczne: architektura wieloprocesowa

Na nowoczesnych urządzeniach z Androidem WebView używa modelu wieloprocesowego, aby zwiększyć bezpieczeństwo i stabilność. Gdy aplikacja używa WebView, pamięć jest rozdzielana między różne procesy:

  1. Proces przeglądarki (proces aplikacji): jest to główny proces aplikacji. Zawiera obiekt Java WebView i część „przeglądarkową” silnika Chromium. Ten proces zarządza interfejsem, żądaniami sieciowymi i renderowaniem GPU (zintegrowanym bezpośrednio z potokiem renderowania Android HWUI). W przeciwieństwie do Chrome WebView nie ma oddzielnego procesu GPU.
  2. Proces renderowania: ten proces jest odpowiedzialny za analizowanie kodu HTML, wykonywanie JavaScriptu i układ. Jest on odizolowany od reszty systemu ze względów bezpieczeństwa. Obecnie aplikacje otrzymują tylko 1 proces renderowania dla wszystkich komponentów WebView (z wyjątkiem kilku rzadkich przypadków specjalnych). W przeciwieństwie do Chrome, który często używa oddzielnych procesów renderowania dla różnych witryn.

Architektura WebView

Dlaczego jest to ważne w kontekście pamięci

Gdy używasz polecenia dumpsys meminfo <your_package>, widzisz tylko pamięć używaną przez proces przeglądarki (proces aplikacji). Pamięć używana przez proces renderowania jest rozliczana oddzielnie.

W procesie przeglądarki pamięć WebView jest rozdzielana w następujący sposób:

  • Stos Java: zawiera otokę Java WebView i powiązane obiekty.
  • Stos natywny: zawiera wewnętrzne struktury danych , pamięć podręczną i stan silnika przeglądarki Chromium. Pamiętaj, że ze względu na użycie PartitionAlloc niektóre natywne alokacje WebView mogą nie być uwzględniane w sekcji „Native Heap” w dumpsys meminfo i mogą zamiast tego pojawiać się w sekcji „Other” lub „Unknown”.
  • Pamięć współdzielona: służy do udostępniania buforów graficznych i innych danych. Może nie być wyraźnie sklasyfikowana przez dumpsys meminfo.

Narzędzia do rozwiązywania problemów

Chrome DevTools

Najpotężniejszym narzędziem do analizowania pamięci wewnątrz WebView (procesu renderowania) są Narzędzia deweloperskie w Chrome.

  1. Włącz debugowanie WebView w aplikacji:

    // NOTE: In production, this should be gated behind a developer setting
    // or only enabled for debuggable builds to prevent reverse engineering.
    WebView.setWebContentsDebuggingEnabled(true);
    
  2. Podłącz urządzenie przez USB.

  3. Otwórz Chrome na komputerze hosta i przejdź do chrome://inspect/#devices.

  4. Znajdź swoją aplikację i kliknij inspect.

  5. W oknie Narzędzi deweloperskich otwórz kartę Memory (Pamięć), aby zrobić zrzuty sterty lub zarejestrować osie czasu alokacji dla sterty JavaScriptu.

dumpsys meminfo

Aby zobaczyć podział pamięci, użyj polecenia adb shell dumpsys meminfo --all <package>. W danych wyjściowych poszukaj kategorii WebView i liczby obiektów.

Profilowanie renderera

Ponieważ renderer działa w osobnym procesie, nie możesz profilować jego sterty natywnej, profilując tylko swoją aplikację. Musisz zidentyfikować PID procesu renderowania.

Aby zidentyfikować prawidłowy PID renderera, gdy aktywnych jest kilka komponentów WebView:

  1. Użyj dumpsys activity:

    adb shell dumpsys activity processes <your_package_name>
    

    Poszukaj sekcji mConnections. Zobaczysz ConnectionRecord łączący Twoją aplikację z SandboxedProcessService. PID tego procesu to Twój renderer. Przykład:

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. Sprawdź nazwy procesów: procesy renderowania mają zwykle nazwy com.google.android.webview:sandboxed_processX lub podobne. Jeśli tylko 1 aplikacja używa WebView, prawdopodobnie będzie tylko 1 proces.

Gdy masz już PID, możesz profilować go za pomocą heapprofd.

Sprawdzone metody zarządzania pamięcią WebView

Jawne niszczenie

Aplikacje powinny wywoływać WebView.destroy(), aby wskazać, kiedy kończą korzystanie z instancji.

WebView próbuje zapewnić, że instancje mogą być zbierane przez moduł odśmiecania i automatycznie zwalniać wszystkie swoje zasoby, ale nie można tego zagwarantować w 100% przypadków. Nawet jeśli automatyczne odśmiecanie pamięci działa, może być znacznie opóźnione, co spowoduje, że aplikacja będzie przechowywać zasoby znacznie dłużej niż oczekiwano.

Jeśli aplikacja wywoła WebView.destroy() w odpowiednim momencie (np. w Activity.onDestroy()), przechowywanie odniesienia do samego obiektu WebView nie spowoduje wycieku żadnych istotnych zasobów natywnych. Nie ma ścisłej potrzeby ustawiania wartości null dla odniesień do obiektu WebView w polach aktywności po jego zniszczeniu, ponieważ zostanie on wyczyszczony, gdy sama aktywność zostanie zebrana przez moduł odśmiecania.

Ćwiczenia: praktyczne korzystanie z pamięci WebView

Ćwiczenie 1. Obserwowanie zajętości pamięci przez procesy

  1. Uruchom MemoryLab i wykonaj pomiar bazowy pamięci aplikacji:

    adb shell dumpsys meminfo com.android.memorylab
    

    Przykładowa linia bazowa (rango): TOTAL PSS: 18915 KB

  2. Kliknij Launch WebView (Normal) (Uruchom WebView (normalny)).

  3. W WebView kilka razy kliknij Allocate JS Memory (1000 DIVs) (Przydziel pamięć JS (1000 DIV-ów)).

  4. Ponownie sprawdź pamięć aplikacji:

    adb shell dumpsys meminfo com.android.memorylab
    
  5. Zauważ, że pamięć w procesie aplikacji nie zwiększa się znacząco w porównaniu z linią bazową. Dzieje się tak, ponieważ elementy DOM znajdują się w procesie renderowania.

  6. Znajdź proces renderowania:

    adb shell ps -A | grep webview | grep sandboxed
    

    Przykładowe dane wyjściowe:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. Sprawdź pamięć procesu renderowania (za pomocą jego PID):

    adb shell dumpsys meminfo 14227
    
  8. Zauważ wysoki TOTAL PSS procesu renderowania. W naszym przykładowym uruchomieniu po kilku alokacjach wzrosła ona do ~55 MB. Pamiętaj, że alokacje JavaScriptu (obsługiwane przez silnik V8) zwykle przyczyniają się do sekcji Private Other (Prywatne inne) lub Unknown (Nieznane) (mmap) w dumpsys meminfo, a nie do sterty Dalvik.

Ćwiczenie 2. Wyciek WebView po stronie Javy

Częstym błędem jest przechowywanie instancji WebView w polu statycznym lub w obiekcie o długim czasie życia, który powoduje wyciek. Ponieważ obiekt WebView jest „kotwicą” o dużej wadze, która przechowuje zasoby natywne i potencjalnie całe procesy renderowania, jego wyciek jest bardzo kosztowny.

Wpływ wycieku WebView

  1. W MemoryLab kliknij Launch WebView (Java Leak) (Uruchom WebView (wyciek Javy)).
  2. Aktywność zostanie automatycznie zamknięta po załadowaniu strony (symulując powtarzającą się nawigację i gromadzenie wycieków).
  3. Kliknij przycisk 4 razy.
  4. Sprawdź liczbę instancji WebView w aplikacji:

    adb shell dumpsys meminfo com.android.memorylab
    

    U dołu poszukaj sekcji Objects (Obiekty). Zobaczysz, że liczba WebViews wzrosła do 4.

    Przykładowe dane wyjściowe (4 wyciekające instancje) na rango:

     Objects
               Views:       51         ViewRootImpl:        5
         AppContexts:       14           Activities:        5
              Assets:       38        AssetManagers:        0
       Local Binders:       55        Proxy Binders:       77
       Parcel memory:       41         Parcel count:       68
    Death Recipients:        3             WebViews:        4
    
  5. Zrób zrzut sterty i użyj AHAT , aby znaleźć wyciek. Jeśli nie masz ahat w ścieżce, możesz go skompilować z drzewa Androida:

    # Dump heap from device
    adb shell am dumpheap com.android.memorylab /data/local/tmp/heap.hprof
    adb pull /data/local/tmp/heap.hprof
    # Run ahat using the built JAR (found in out/host/linux-x86/framework/)
    java -jar out/host/linux-x86/framework/ahat.jar -p 8888 heap.hprof
    
  6. W interfejsie AHAT (localhost:8888) kliknij link allocations (alokacje) (lub sites (witryny)) w górnym menu, aby zobaczyć ogólne wykorzystanie pamięci.

    Przydziały AHAT

  7. Wyszukaj klasę android.webkit.WebView. Kliknij instance count (liczbę instancji), aby zobaczyć wszystkie aktywne instancje. Na liście powinno się wyświetlić kilka instancji.

    Instancje WebView AHAT

  8. Kliknij jedną z wyciekających instancji WebView. Przewiń w dół do sekcji Sample Path from GC Root (Przykładowa ścieżka od głównego elementu odśmiecania). Zobaczysz, że jest ona przechowywana na liście sLeakedWebViews w com.android.memorylab.WebViewActivity.

    Ścieżka AHAT do głównego elementu GC


← Native | ↑ Up | App code →