WebView-Speicher verwalten und analysieren

WebView führt nativen Code in mehreren Prozessen aus, um Webinhalte in Ihrer Android-App zu rendern. Wenn WebView-Instanzen nicht verwaltet werden, kann dies zu Speicherlecks, Abstürzen aufgrund von Arbeitsspeichermangel und einer schlechteren App-Leistung führen.

In diesem Dokument wird das WebView-Speichermodell mit mehreren Prozessen erläutert. Außerdem wird beschrieben, wie Sie den Lebenszyklus richtig verwalten, um Lecks zu vermeiden, und es werden praktische Workflows zur Diagnose von Speicherproblemen bereitgestellt.

Speicherarchitektur von WebView

Um den WebView-Speicher effektiv zu verwalten, müssen Sie wissen, wie Android Ressourcen für Webinhalte zuweist:

  • Ausführung in mehreren Prozessen:Unter Android 8.0 (API-Level 26) und höher trennt WebView Web-Inhalte von den Kernfunktionen Ihrer App in mehreren Prozessen (auf Geräten mit wenig RAM wird möglicherweise auf einen einzelnen Prozess zurückgegriffen):

    • Hostprozess (Browserprozess):Der Haupt-App-Prozess, in dem Ihr Activity- und Java- oder Kotlin-Code ausgeführt wird.
    • Isolierter Renderer-Prozess:Ein separater Sandbox-Prozess (SandboxedProcessService), in dem HTML und CSS geparst, JavaScript ausgeführt und Webseiten gerendert werden.
  • Nativer Speicherbedarf:Der größte Teil des WebView-Speichers, einschließlich gerenderter Grafiken, des DOM-Baums und des JavaScript-Laufzeitspeichers, wird im nativen Speicher und nicht im Java-Heap zugewiesen. Ein Java-Heap-Dump (.hprof) zeigt nur ein einfaches Java-Wrapper-Objekt und nicht den tatsächlichen Arbeitsspeicher, der von Webinhalten verwendet wird.

  • Systemauswirkungen von nativem Arbeitsspeicher:Im Gegensatz zu Java-Heap-Zuweisungen, die durch das maxHeap-Limit der App begrenzt sind und schnell mit einem OutOfMemoryError fehlschlagen, kann der native Arbeitsspeicher unbemerkt auf Gigabyte anwachsen. Wenn nicht freigegebener nativer Arbeitsspeicher den physischen Arbeitsspeicher und den Auslagerungsspeicher (zRAM) füllt, beginnt der Low Memory Killer (LMK) von Android, Hintergrundprozesse zu beenden, um Arbeitsspeicher freizugeben. Dadurch wird das Multitasking auf dem Gerät insgesamt beeinträchtigt, bevor die App im Vordergrund beendet wird.

WebView-Lebenszyklus verwalten

Eine ordnungsgemäße Lebenszyklusverwaltung ist entscheidend, um Speicherlecks zu vermeiden. Ein häufiger Fehler ist die Annahme, dass durch das Entfernen eines WebView aus dem Layout oder das automatische Beenden eines Activity der Speicher freigegeben wird.

Damit sowohl Java-Kontextreferenzen als auch native Renderressourcen vollständig bereinigt werden, müssen Sie explizit eine Abbaufolge im Lebenszyklus der Hostkomponente (z. B. onDestroy()) orchestrieren. Dabei müssen Sie die aktive Seitenausführung beenden, die Ansicht von ihrem Container trennen und native Bindungen freigeben.

WebView-Instanzen bereinigen

Damit das Herunterfahren sauber erfolgt und Ressourcen freigegeben werden, wenn Ihre Activity oder Fragment zerstört wird, gehen Sie so vor:

  1. Entfernen Sie WebView aus dem übergeordneten Container (ViewGroup).
  2. Aktives Laden beenden und Navigationsverlauf löschen.
  3. Rufen Sie destroy() an.
  4. Entfernen Sie den Verweis auf null.

Das folgende Beispiel zeigt, wie eine WebView richtig bereinigt wird:

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();
}

Grundlegendes zum Speicher nach der Vernichtung

Wenn Sie destroy() aufrufen, gibt das System den Activity-Kontext frei, bereinigt die Ansichtshierarchien und beendet die Web-Hintergrundarbeit. Möglicherweise stellen Sie jedoch fest, dass der physische Speicher des Prozesses (Resident Set Size) nicht sofort auf den Pre-WebView-Ausgangswert sinkt.

Das ist aber normal. Native Laufzeit-Caches, gemeinsam genutzte Bibliotheken und zugewiesene Speicherseiten bleiben im Prozess, bis das Betriebssystem sie zurückfordert oder der Prozess beendet wird. Das primäre Ziel von destroy() ist es, kumulative Activity-Speicherlecks zu verhindern, wenn Nutzer auf webbasierten Bildschirmen navigieren.

Wichtige Messwerte für das Debugging

Konzentrieren Sie sich bei der Analyse des WebView-Speicherverbrauchs auf die folgenden Messwerte:

  • Resident Set Size (RSS): Der gesamte physische RAM, der dem Prozess zugeordnet ist, einschließlich gemeinsam genutzten Codes und Bibliotheken (im Android Studio Profiler als Total gekennzeichnet).

  • Anonymer RSS (RssAnon): Speicher, der direkt vom Prozess zugewiesen wird und nicht durch eine Datei auf der Festplatte gesichert wird, z. B. Zuweisungen für den nativen Heap und die JavaScript-Laufzeit. Dies sind die primären Speicherkosten Ihrer Webinhalte (im Android Studio Profiler als Allocated gekennzeichnet).

  • Privater Speicherbedarf (Private Memory Footprint, PMF): Die Summe aus anonymer RSS und Auslagerung (zRAM). Der PMF gibt die tatsächliche Arbeitsspeicherbelastung an, die Ihre App für das System darstellt und die nicht ausgelagert werden kann.

  • Browser-PMF im Vergleich zu Renderer-PMF:Arbeitsspeicher, der vom Hauptprozess Ihrer App verwendet wird, im Vergleich zu Arbeitsspeicher, der vom isolierten Renderer-Prozess verwendet wird. Umfangreiche Webinhalte verursachen vor allem im Renderer-Prozess Spitzen.

  • Live Object Counts (WebViews, Activities, Views): Die Anzahl der aktiven UI-, Kontext- und WebView-Instanzen, die im Arbeitsspeicher gehalten werden. Durch das Tracking dieser Werte lässt sich feststellen, ob der Speicherzuwachs durch beibehaltene Java-Referenzen oder nur durch native Zuweisungen verursacht wird.

  • Privater „Other“-Speicher und nativer Heap:In dumpsys meminfo werden native C/C++-Zuweisungen und benutzerdefinierte Speicherzuordnungen (z. B. Chromium PartitionAlloc oder eingebettete JavaScript-Laufzeit-Heaps) unter „Nativer Heap“ und „Privater Other“-Speicher anstatt unter „Java-Heap“ angezeigt.

Weitere Informationen zu Prozessspeicherzählern und ihren Kategorien finden Sie im Glossar zum Prozessspeicher.

Praktische Diagnose-Workflows

Da WebView in mehreren Prozessen ausgeführt wird und nativen Speicher zuweist, können Sie den Speicherbedarf mit den folgenden Tools und Techniken untersuchen:

Tools zur Profilerstellung und Diagnose

Verwenden Sie die folgenden Tools, um Speicherzuweisungen zu prüfen und Lecks zu diagnostizieren:

  • Speicher-Profiler von Android Studio:Mit dem Speicher-Profiler können Sie native Zuweisungen visualisieren, Arbeitsspeicherkategorien im Zeitverlauf verfolgen und Activity-Leaks bei Bildschirmübergängen erkennen.

  • Arbeitsspeicher mit Perfetto im Blick behalten:Verwenden Sie Perfetto, um Arbeitsspeicherzähler auf Systemebene (z. B. RSS und anonyme RSS) aufzuzeichnen und so das gesamte Arbeitsspeicherwachstum zu beobachten. Bei WebView-Zuweisungen der nativen Engine werden im Heap-Profilerstellungstool von Perfetto keine Callstacks erstellt. Verwenden Sie die Chrome-Entwicklertools, um JavaScript-Heap-Snapshots und DOM-Zuweisungen in den Webinhalten zu untersuchen.

Anzahl der Live-Objekte prüfen

Um festzustellen, ob das Speicherwachstum durch beibehaltene Java-Framework-Objekte (z. B. UI-Komponenten) oder native Zuweisungen verursacht wird, sehen Sie sich den Bereich Objects von dumpsys meminfo an:

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

In der Ausgabe werden die Anzahl der Live-Objekte angezeigt:

 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

In diesem Abschnitt werden die Anzahl der aktiven Framework-Objekte, IPC-Handles und Parcel-Zuweisungen angezeigt. Konzentrieren Sie sich bei der WebView-Diagnose hauptsächlich auf Activities und WebViews.

Führen Sie die Zielnutzerinteraktion (z. B. Öffnen und Schließen eines Webbildschirms) wiederholt aus und vergleichen Sie die Zählungen:

  • Instanz-Leak:Wenn WebViews oder Activities bei jeder Navigation ansteigt und nicht zum Ausgangswert zurückkehrt, gibt es in Ihrer App einen Leak der Java-WebView-Instanz oder des Hosts Activity (z. B. aufgrund eines fehlenden ViewGroup.removeView() oder beibehaltener Listener-Referenzen). Da ein Leak von Activity den gesamten Ansichtsbaum und die decodierten Bildressourcen im Arbeitsspeicher fixiert, wird der Java-Heap bei wiederholten Besuchen schnell erschöpft und es kommt zu OutOfMemoryError-Abstürzen.

  • Native- oder DOM-Leck: Wenn WebViews und Activities konstant bleiben, während die gesamte Prozess-RSS und Private Other weiter ansteigen, stammt das Leck von nicht freigegebenen nativen Ressourcen, DOM-Elementen oder JavaScript-Engine-Bindungen. Da sich diese Zuweisungen im nativen Speicher befinden und den ART-Garbage Collector umgehen, sind sie für Standardtools zur Erkennung von Java-Speicherlecks unsichtbar und werden immer mehr, bis das Betriebssystem die App beendet.

Isolierten Renderer-Prozess mit der CLI profilieren

Wenn Sie dumpsys meminfo mit dem Paketnamen Ihrer App ausführen, wird nur der Arbeitsspeicher für den Haupt-Hostprozess ausgegeben. So prüfen Sie den isolierten Renderingprozess, in dem Webseiten gerendert werden:

  1. Suchen Sie die Prozess-ID (PID) des isolierten Renderer-Dienstes:

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

    In der Ausgabe wird der isolierte Prozessdatensatz und seine PID RENDERER_PID (z. B. 22155) angezeigt:

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Sehen Sie sich die Arbeitsspeicheraufschlüsselung des Renderer-Prozesses anhand seiner PID an:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. Untersuchen Sie den Host-App-Prozess, um die Auswirkungen auf den Browser zu bewerten:

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

Arbeitsspeicherzuordnungen und -belegungen prüfen

Um herauszufinden, welche nativen Subsysteme oder Zuweisungen anonymen Speicher belegen, sehen Sie sich die Prozessspeicherkarten an:

adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"

In der folgenden Tabelle sind häufige anonyme Arbeitsspeicher-Tags und ihre Relevanz für die Arbeitsspeichererweiterung aufgeführt:

Erinnerungs-Tag Subsystem Relevanz für App- und Webinhalte Häufige Ursache für erhöhten Arbeitsspeicher?
[anon:partition_alloc] Chromium PartitionAlloc Zuweisungen für DOM-Bäume, Rendering-Puffer, V8-JavaScript-Heap und WebAssembly-Ausführung in WebView. Ja (Hoch): Das Laden umfangreicher Webseiten, DOMs mit vielen Medien oder das Versäumnis, destroy() für verworfene WebView-Instanzen direkt aufzurufen, führt zu einer Erhöhung dieses Tags.
[anon:scudo...] oder [anon:libc_malloc] Native Heap-Zuweisungen für Android (Scudo / jemalloc) Allgemeine native C/C++-Zuweisungen, die von NDK-Bibliotheken, JNI-Bridges und nativen Grafikpipelines verwendet werden. Ja (mittel bis hoch): Das Wachstum tritt auf, wenn native JNI-Wrapper oder C++-Drittanbieterabhängigkeiten nicht freigegebene Zuweisungen bei Navigationsvorgängen beibehalten.
[anon:...] (z. B. [anon:quickjs_heap...]) Benutzerdefinierte Scripts oder native Laufzeiten Eingebettete JavaScript-Engines, benutzerdefinierte WebAssembly-Laufzeitumgebungen oder benutzerdefinierte native Pufferpools. Ja (kontextabhängig): Häufig bei Hybrid-Apps, die Scripting-Engines neben nativen Ansichten ausführen und Laufzeitbindungen nicht bereinigen.

Einschränkungen der In-App-Speicher-APIs

In-App-Speicher-APIs (z. B. Debug.getMemoryInfo oder ActivityManager.getProcessMemoryInfo) messen nur den aufrufenden Prozess. Im Mehrprozessmodus können diese APIs den vom isolierten Rendererprozess belegten Arbeitsspeicher nicht erfassen. Für eine genaue Bewertung des Gesamtspeichers sollten Sie Systemtools wie dumpsys meminfo, Perfetto oder Android Studio Profiler verwenden.

Hohe Arbeitsspeichernutzung in einer Hybrid-App untersuchen

Wenn Sie ein unerklärliches Anwachsen des Arbeitsspeichers bei wiederkehrenden WebView-Interaktionen (z. B. beim Öffnen von Weblinks oder beim Navigieren in webbasierten Feeds) diagnostizieren, verwenden Sie den folgenden Triage-Workflow, um zu ermitteln, ob das Leck in der Java-Ebene oder in der nativen Engine auftritt:

  1. Art des Lecks isolieren (Java oder nativ): Führen Sie dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" vor und nach wiederholten Nutzerübergängen aus, z. B. beim Öffnen und Schließen von Webartikeln oder beim Wischen durch Feeds.

    • Beobachtung:Wenn die Anzahl von Activities- und WebViews-Zählern konstant bleibt (z. B. 1–2 aktive Instanzen), gibt es in der App keine Lecks bei Activity-Kontexten oder Java-WebView-Instanzen.
  2. Speicherdelta über Interaktionen hinweg messen (Zeitreihen-Tracking): Erfassen Sie dumpsys meminfo-Momentaufnahmen über mehrere Nutzerinteraktionen hinweg, um die Zuweisungsrate pro Übergang zu berechnen:

    • Beobachtung:Der Java-Heap bleibt begrenzt und in gutem Zustand (er steigt bei der Verwendung und sinkt nach der Garbage Collection), aber Private Other und Native Heap steigen bei jedem Übergang um mehrere Megabyte. Das beweist, dass sich das Speicherleck vollständig im nativen Speicher außerhalb der ART-Laufzeit befindet. Standardmäßige Java-Heap-Dumps (.hprof) zeigen keine Probleme.
  3. Anonyme Arbeitsspeicherzuordnungen prüfen:Untersuchen Sie die Arbeitsspeicherzuordnungen des Prozesses mit ADB (siehe Arbeitsspeicherzuordnungen und ‑zuweisungen prüfen):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Beobachtung:Der Arbeitsspeicherverbrauch steigt hauptsächlich in den Heaps der [anon:partition_alloc]- oder eingebetteten Scripting-Engine. Gleichzeitig steigt die Anzahl der globalen JNI-Referenzen langsam an. Das bedeutet, dass die Java-Ansichten zwar ersetzt wurden, die zugrunde liegenden nativen Seitenobjekte oder JavaScript-Bindungen jedoch nicht freigegeben wurden.
  4. Abhilfe:

    • Achten Sie darauf, dass bei jedem recycelten oder entsorgten WebView aktive Skripts (stopLoading()) explizit beendet, der Verlauf gelöscht und destroy() aufgerufen wird.
    • Rufen Sie benutzerdefinierte JavaScript-Bridge-Callbacks oder globale JNI-Referenzen auf, die mit geschlossenen Ansichten verknüpft sind.
    • Prüfen Sie, ob Private Other und der Prozess-RSS nach Navigationsübergängen stabilisiert werden.

Zusätzliche Ressourcen

Weitere Informationen zum Debuggen und Profilieren von Arbeitsspeicher und WebView-Leistung finden Sie in den folgenden Ressourcen: