WebView-Speicher verwalten und analysieren

WebView führt nativen Code über mehrere Prozesse aus, um Webinhalte in Ihrer Android-App zu rendern. Wenn WebView-Instanzen nicht verwaltet werden, kann dies zu Speicherlecks, Abstürzen aufgrund von Out of Memory (OOM) und einer beeinträchtigten App-Leistung führen.

In diesem Dokument wird das Multiprozess-Speichermodell von WebView erläutert, beschrieben, wie Sie den Lebenszyklus richtig verwalten, um Lecks zu vermeiden, und praktische Workflows zur Diagnose von Speicherproblemen bereitgestellt.

WebView-Speicherarchitektur verstehen

Wenn du den WebView-Arbeitsspeicher effektiv verwalten möchtest, solltest du wissen, wie Android Ressourcen für Webinhalte zuweist:

  • Ausführung mehrerer Prozesse:Unter Android 8.0 (API-Level 26) und höher trennt WebView Webinhalte von den Hauptfunktionen Ihrer App über mehrere Prozesse hinweg. Auf Geräten mit wenig RAM wird möglicherweise auf einen einzelnen Prozess zurückgegriffen:

    • Hostprozess (Browserprozess):Der Hauptprozess der App, in dem Ihr Activity- und Java- oder Kotlin-Code ausgeführt wird.
    • Isolierter Renderer-Prozess:Ein separater Sandbox-Prozess (SandboxedProcessService), der HTML und CSS parst, JavaScript ausführt und Webseiten rendert.
  • Nativer Speicherbedarf:Der Groß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 erfasst nicht den tatsächlichen Speicher, der von Webinhalten verwendet wird.

  • Auswirkungen des nativen Speichers auf das System:Im Gegensatz zu Java-Heap-Zuweisungen, die durch das maxHeap-Limit der App begrenzt sind und schnell mit einem OutOfMemoryError fehlschlagen, kann der native Speicher unbemerkt auf Gigabyte anwachsen. Wenn nicht freigegebener nativer Arbeitsspeicher den physischen RAM 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 beeinträchtigt, bevor die App im Vordergrund schließlich beendet wird.

WebView-Lebenszyklus verwalten

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

Damit sowohl Java-Kontextreferenzen als auch native Rendering-Ressourcen vollständig bereinigt werden, müssen Sie im Lebenszyklus Ihrer Hostkomponente (z. B. onDestroy()) explizit eine Teardown-Sequenz einrichten. Dadurch wird die Ausführung der aktiven Seite beendet, die Ansicht vom Container getrennt und native Bindungen werden freigegeben.

WebView-Instanzen bereinigen

So sorgen Sie für ein sauberes Herunterfahren und geben Ressourcen frei, wenn Ihr Activity oder Fragment gelöscht wird:

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

Das folgende Beispiel zeigt, wie Sie eine WebView ordnungsgemäß bereinigen:

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

Informationen zu gemerkten Informationen nach dem Löschen

Wenn Sie destroy() aufrufen, gibt das System den Activity-Kontext frei, bereinigt Ansichtshierarchien und beendet die Hintergrundarbeit im Web. Möglicherweise stellen Sie jedoch fest, dass der physische Speicher des Prozesses (Resident Set Size) nicht sofort auf den Wert vor der WebView-Nutzung 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 Hauptziel von destroy() besteht darin, kumulative Activity-Speicherlecks zu verhindern, wenn Nutzer zwischen webbasierten Bildschirmen wechseln.

Wichtige Messwerte für die Fehlerbehebung

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

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

  • Anonyme RSS (RssAnon): Speicher, der direkt vom Prozess zugewiesen wird und nicht durch eine Datei auf dem Laufwerk gesichert ist (z. B. nativer Heap und JavaScript-Laufzeitzuweisungen). Dies sind die primären Arbeitsspeicherkosten Ihrer Webinhalte (im Android Studio Profiler als Zugewiesen gekennzeichnet).

  • Privater Speicherbedarf (PMF): Die Summe aus anonymer RSS und Auslagerung (zRAM). Der PMF spiegelt die tatsächliche nicht entfernbare Arbeitsspeicherbelastung wider, die Ihre App auf das System ausübt.

  • Browser-PMF im Vergleich zu Renderer-PMF:Vom Hauptprozess Ihrer App verwendeter Arbeitsspeicher im Vergleich zum vom isolierten Renderer-Prozess verwendeten Arbeitsspeicher. Umfangreiche Webinhalte verursachen vor allem im Renderer-Prozess Spitzen.

  • Anzahl der Live-Objekte (WebViews, Activities, Views): Die Anzahl der aktiven UI-, Kontext- und WebView-Instanzen im Speicher. Durch das Tracking dieser Werte können Sie feststellen, ob die Zunahme des Arbeitsspeichers durch beibehaltene Java-Referenzen oder nur durch native Zuweisungen verursacht wird.

  • Privater anderer 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 anderer“ statt unter „Java-Heap“ angezeigt.

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

Praktische Diagnose-Workflows

Da WebView über mehrere Prozesse hinweg ausgeführt wird und nativen Speicher zuweist, sollten Sie die folgenden Tools und Techniken verwenden, um den Speicherbedarf zu prüfen:

Tools zur Profilerstellung und Diagnose

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

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

  • Speicher-Tracking mit Perfetto: Verwenden Sie Perfetto, um Speicherzähler auf Systemebene (z. B. RSS und anonyme RSS) aufzuzeichnen und das allgemeine Speicherwachstum zu beobachten. Beachten Sie, dass WebView-native Engine-Zuweisungen keine Aufrufstacks im Heap-Profiling-Tool von Perfetto erzeugen. Verwenden Sie die Chrome-Entwicklertools, um JavaScript-Heap-Snapshots und DOM-Zuweisungen im Webinhalt zu prüfen.

Anzahl der Live-Objekte prüfen

Wenn Sie feststellen möchten, ob der Speicherzuwachs durch beibehaltene Java-Framework-Objekte (z. B. UI-Komponenten) oder native Zuweisungen verursacht wird, prüfen Sie den Abschnitt Objects von dumpsys meminfo:

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

In der Ausgabe wird 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 wird 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. das Öffnen und Schließen eines Webbildschirms) wiederholt aus und vergleichen Sie die Anzahl:

  • Instanzleck: Wenn WebViews oder Activities bei jeder Navigation zunimmt und nicht auf den Ausgangswert zurückkehrt, liegt in Ihrer App ein Leck der Java-WebView-Instanz oder des Hosts Activity vor (z. B. aufgrund eines fehlenden ViewGroup.removeView() oder beibehaltenen Listener-Verweisen). Da bei einem Speicherleck in Activity der gesamte Ansichtsbaum und die decodierten Bildressourcen im Speicher verankert werden, wird der Java-Heap bei wiederholten Besuchen schnell erschöpft und es kommt zu OutOfMemoryError-Abstürzen.

  • Natives oder DOM-Leck:Wenn WebViews und Activities konstant bleiben, während der RSS-Wert des gesamten Prozesses und Private Other weiter steigen, stammt das Leck von nicht freigegebenen nativen Ressourcen, DOM-Elementen oder JavaScript-Engine-Bindungen. Da sich diese Zuordnungen im nativen Speicher befinden und den ART-Garbage-Collector umgehen, bleiben sie für Standard-Java-Tools zur Erkennung von Speicherlecks unsichtbar und sammeln sich weiter an, bis das Betriebssystem die App beendet.

Isolierten Renderer-Prozess über die Befehlszeile 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 Renderer-Prozess, 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 werden der isolierte Prozesseintrag und seine PID RENDERER_PID (z. B. 22155) angezeigt:

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Prüfen Sie die Speicheraufschlüsselung des Renderer-Prozesses anhand seiner PID:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. Prüfen Sie den Host-App-Prozess, um den browserseitigen Speicherbedarf zu ermitteln:

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

Speicherzuordnungen und -zuweisungen prüfen

Wenn Sie herausfinden möchten, welche nativen Subsysteme oder Allocators anonymen Speicher belegen, prüfen Sie die Prozessspeicherzuordnungen:

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

In der folgenden Tabelle sind gängige anonyme Speicher-Tags und ihre Relevanz für das Speicherwachstum aufgeführt:

Tag für gemerkte Informationen Subsystem Relevanz für App- und Webinhalte Häufige Ursache für die Erhöhung des Arbeitsspeichers?
[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, medienreicher DOMs oder das Versäumnis, destroy() für verworfene WebView-Instanzen aufzurufen, führt direkt zu einer Erhöhung dieses Tags.
[anon:scudo...] oder [anon:libc_malloc] Native Android-Heap-Zuweisungen (Scudo / jemalloc) Allgemeine native C/C++-Zuweisungen, die von NDK-Bibliotheken, JNI-Brücken und nativen Grafikpipelines verwendet werden. Ja (mittel bis hoch): Ein Wachstum tritt auf, wenn native JNI-Wrapper oder C++-Abhängigkeiten von Drittanbietern nicht freigegebene Zuweisungen über Navigationsvorgänge hinweg beibehalten.
[anon:...] (z. B. [anon:quickjs_heap...]) Benutzerdefiniertes Scripting oder native Laufzeiten Eingebettete JavaScript-Engines, benutzerdefinierte WebAssembly-Laufzeiten oder benutzerdefinierte native Pufferpools. Ja (kontextabhängig): Häufig in Hybrid-Apps, die Scripting-Engines zusammen mit nativen Ansichten ausführen und Laufzeitbindungen nicht bereinigen.

Einschränkungen von In-App-Memory-APIs

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

Hohe Speicherauslastung in einer Hybrid-App priorisieren

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

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

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

    • Beobachtung: Der Java-Heap bleibt begrenzt und fehlerfrei (Spitzen während der Nutzung und Abfall nach der automatischen Speicherbereinigung), aber Private Other und Native Heap steigen pro Übergang stetig um mehrere Megabyte an. Dies beweist, dass sich das Leck vollständig im nativen Speicher außerhalb der ART-Laufzeit befindet. In Standard-Java-Heap-Dumps (.hprof) werden keine Probleme angezeigt.
  3. Anonyme Speicherzuordnungen prüfen:Prüfen Sie die Prozessspeicherzuordnungen mit ADB (siehe Speicherzuordnungen und ‑zuweisungen prüfen):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Beobachtung: Das Speicherwachstum konzentriert sich auf [anon:partition_alloc] oder eingebettete Scripting-Engine-Heaps, begleitet von einem langsamen Anstieg der globalen JNI-Referenzen. Dies weist darauf hin, dass die Java-Ansichten zwar ersetzt, die zugrunde liegenden nativen Seitenobjekte oder JavaScript-Bindungen jedoch nicht freigegeben wurden.
  4. Abhilfe:

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

Zusätzliche Ressourcen

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