Gestire e diagnosticare la memoria di WebView

WebView esegue codice nativo in più processi per eseguire il rendering dei contenuti web nella tua app Android. Se lasci le istanze WebView non gestite, potresti riscontrare perdite di memoria, arresti anomali per memoria insufficiente (OOM) e un peggioramento del rendimento dell'app.

Questo documento spiega il modello di memoria multi-processo di WebView, descrive come gestire correttamente il suo ciclo di vita per evitare perdite e fornisce workflow pratici per la diagnosi dei problemi di memoria.

Informazioni sull'architettura della memoria di WebView

Per gestire in modo efficace la memoria di WebView, devi capire come Android alloca le risorse per i contenuti web:

  • Esecuzione multi-processo: su Android 8.0 (livello API 26) e versioni successive, WebView separa i contenuti web dalle funzioni principali dell'app in più processi (sui dispositivi con poca RAM, potrebbe tornare a un singolo processo):

    • Processo host (browser): il processo dell'app principale in cui vengono eseguiti il codice Activity e Java o Kotlin.
    • Processo di rendering isolato: un processo sandbox separato (SandboxedProcessService) che analizza HTML e CSS, esegue JavaScript ed esegue il rendering delle pagine web.
  • Impronta di memoria nativa: la maggior parte della memoria di WebView, inclusi la grafica sottoposta a rendering, l'albero DOM e la memoria di runtime di JavaScript, viene allocata nella memoria nativa, non nell'heap Java. Un dump dell'heap Java (.hprof) mostra solo un oggetto wrapper Java leggero e non acquisisce la memoria effettiva utilizzata dai contenuti web.

  • Impatto della memoria nativa sul sistema: a differenza delle allocazioni dell'heap Java, che sono limitate dal limite maxHeap dell'app e non vanno a buon fine rapidamente con un OutOfMemoryError, la memoria nativa può aumentare silenziosamente fino a raggiungere gigabyte. Man mano che la memoria nativa non rilasciata riempie la RAM fisica e lo spazio di swap (zRAM), Low Memory Killer (LMK) di Android inizia a terminare i processi in background per recuperare la memoria. Questo peggiora il multitasking complessivo del dispositivo prima di terminare l'app in primo piano.

Gestire il ciclo di vita di WebView

Una gestione corretta del ciclo di vita è fondamentale per prevenire perdite di memoria. Un errore comune è presupporre che la rimozione di un WebView dal layout o la chiusura automatica di un Activity liberi la memoria.

Per garantire la pulizia completa sia dei riferimenti al contesto Java sia delle risorse di rendering native, devi orchestrare esplicitamente una sequenza di chiusura nel ciclo di vita del componente host (ad esempio onDestroy()), interrompendo l'esecuzione della pagina attiva, scollegando la visualizzazione dal relativo contenitore e rilasciando i binding nativi.

Pulire le istanze WebView

Per garantire un arresto pulito e rilasciare le risorse quando Activity o Fragment viene eliminato:

  1. Rimuovi WebView dal relativo contenitore principale (ViewGroup).
  2. Interrompi il caricamento attivo e cancella la cronologia di navigazione.
  3. Chiama destroy().
  4. Cancella il riferimento a null.

L'esempio seguente mostra come pulire correttamente un 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();
}

Informazioni sulla memoria post-eliminazione

Quando chiami destroy(), il sistema rilascia il contesto Activity, pulisce le gerarchie delle visualizzazioni e interrompe il lavoro in background web. Tuttavia, potresti notare che la memoria fisica del processo (Resident Set Size) non scende immediatamente alla baseline pre-WebView.

Questo comportamento è normale. Le cache di runtime native, le librerie condivise e le pagine di memoria allocate rimangono residenti nel processo finché il sistema operativo non le recupera o il processo non termina. L'obiettivo principale di destroy() è prevenire perdite di memoria cumulative di Activity quando gli utenti navigano nelle schermate basate sul web.

Metriche di debug chiave

Quando analizzi il consumo di memoria di WebView, concentrati sulle seguenti metriche:

  • Resident Set Size (RSS): la RAM fisica totale mappata nel processo, inclusi il codice e le librerie condivisi (etichettati come Totale in Android Studio Profiler).

  • Anonymous RSS (RssAnon): memoria allocata direttamente dal processo che non è supportata da un file su disco (ad esempio allocazioni dell'heap nativo e del runtime di JavaScript). Rappresenta il costo di memoria principale dei contenuti web (etichettato come Allocato in Android Studio Profiler).

  • Private Memory Footprint (PMF): la somma di RSS anonimo e swap (zRAM). PMF riflette il carico di memoria non eliminabile effettivo che la tua app impone al sistema.

  • PMF del browser rispetto a PMF del renderer: memoria utilizzata dal processo principale dell'app rispetto alla memoria utilizzata dal processo di rendering isolato. I contenuti web pesanti causano picchi principalmente nel processo di rendering.

  • Conteggi degli oggetti attivi (WebViews, Activities, Views): il numero di istanze attive di UI, Context e WebView mantenute in memoria. Il monitoraggio di questi elementi consente di identificare se la crescita della memoria è causata da riferimenti Java mantenuti o da allocazioni solo native.

  • Private Other e Native Heap: in dumpsys meminfo, le allocazioni native C/C++ e le mappature di memoria personalizzate (ad esempio gli heap di runtime di Chromium PartitionAlloc o JavaScript incorporati) vengono visualizzate in Native Heap e Private Other anziché in Java Heap.

Per ulteriori informazioni sui contatori della memoria di processo e sulle relative categorie, consulta il glossario della memoria di processo.

Workflow di diagnostica pratici

Poiché WebView opera su più processi e alloca memoria nativa, utilizza i seguenti strumenti e tecniche per esaminare la sua impronta:

Strumenti di profilazione e diagnostica

Per esaminare le allocazioni di memoria e diagnosticare le perdite, utilizza i seguenti strumenti:

  • Android Studio Memory Profiler: utilizza Memory Profiler per visualizzare le allocazioni native, monitorare le categorie di memoria nel tempo e rilevare Activity durante le transizioni tra le schermate.

  • Monitoraggio della memoria con Perfetto: utilizza Perfetto per registrare i contatori di memoria a livello di sistema (ad esempio RSS e RSS anonimo) per osservare la crescita complessiva della memoria. Tieni presente che le allocazioni del motore nativo di WebView non producono stack di chiamate nello strumento di profilazione dell'heap di Perfetto. Utilizza Chrome DevTools per esaminare gli snapshot dell'heap JavaScript e le allocazioni DOM all'interno dei contenuti web.

Esaminare i conteggi degli oggetti attivi

Per determinare se la crescita della memoria è causata da oggetti del framework Java mantenuti (ad esempio componenti UI) o da allocazioni native, esamina la Objects sezione di dumpsys meminfo:

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

L'output mostra i conteggi degli oggetti attivi:

 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

Questa sezione mostra i conteggi degli oggetti del framework attivi, degli handle IPC e delle allocazioni Parcel. Per la diagnostica di WebView, concentrati principalmente su Activities e WebViews.

Esegui ripetutamente l'interazione utente di destinazione (ad esempio l'apertura e la chiusura di una schermata web) e confronta i conteggi:

  • Perdita di istanze: se WebViews o Activities aumenta a ogni navigazione e non torna alla baseline, la tua app sta perdendo l'istanza Java WebView o l'istanza Activity host (ad esempio, a causa di un ViewGroup.removeView() mancante o di riferimenti ai listener mantenuti). Poiché un Activity con perdita di memoria blocca l'intero albero delle visualizzazioni e le risorse immagine decodificate in memoria, le visite ripetute esauriranno rapidamente l'heap Java e causeranno arresti anomali OutOfMemoryError.

  • Perdita nativa o DOM: se WebViews e Activities rimangono costanti mentre l'RSS totale del processo e Private Other continuano ad aumentare, la perdita ha origine da risorse native non rilasciate, elementi DOM o binding del motore JavaScript. Poiché queste allocazioni risiedono nella memoria nativa ed eludono il garbage collector ART, rimangono invisibili agli strumenti standard di rilevamento delle perdite Java e continuano ad accumularsi finché il sistema operativo non termina l'app.

Profilare il processo di rendering isolato utilizzando la CLI

L'esecuzione di dumpsys meminfo con il nome del pacchetto dell'app restituisce solo la memoria per il processo host principale. Per esaminare il processo di rendering isolato in cui vengono sottoposte a rendering le pagine web:

  1. Trova l'ID processo (PID) del servizio di rendering isolato:

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

    L'output mostra il record del processo isolato e il relativo PID RENDERER_PID (ad esempio, 22155):

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Esamina la suddivisione della memoria del processo di rendering utilizzando il relativo PID:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. Esamina il processo dell'app host per valutare l'impronta lato browser:

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

Esaminare le mappe e le allocazioni di memoria

Per vedere quali sottosistemi o allocatori nativi occupano la memoria anonima, esamina le mappe della memoria di processo:

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

La tabella seguente elenca i tag di memoria anonima comuni e la loro pertinenza per la crescita della memoria:

Tag di memoria Sottosistema Pertinenza per l'app e i contenuti web Causa comune dell'aumento della memoria?
[anon:partition_alloc] Chromium PartitionAlloc Allocazioni per alberi DOM, buffer di rendering, heap JavaScript V8 ed esecuzione di WebAssembly in WebView. Sì (alta): il caricamento di pagine web pesanti, DOM ricchi di contenuti multimediali o la mancata chiamata di destroy() sulle istanze WebView eliminate gonfia direttamente questo tag.
[anon:scudo...] o [anon:libc_malloc] Allocatori dell'heap nativo Android (Scudo / jemalloc) Allocazioni native C/C++ generali utilizzate dalle librerie NDK, dai bridge JNI e dalle pipeline grafiche native. Sì (da moderata ad alta): la crescita si verifica quando i wrapper JNI nativi o le dipendenze C++ di terze parti mantengono le allocazioni non rilasciate durante le navigazioni.
[anon:...] (ad esempio, [anon:quickjs_heap...]) Runtime nativi o di scripting personalizzati Motori JavaScript incorporati, runtime WebAssembly personalizzati o pool di buffer nativi personalizzati. Sì (dipende dal contesto): comune nelle app ibride che eseguono motori di scripting insieme a visualizzazioni native e non puliscono i binding di runtime.

Limitazioni delle API di memoria in-app

Le API di memoria in-app (ad esempio Debug.getMemoryInfo o ActivityManager.getProcessMemoryInfo) misurano solo il processo chiamante. In modalità multi-processo, queste API non possono acquisire la memoria consumata dal processo di rendering isolato. Per una valutazione accurata della memoria totale, affidati a strumenti di sistema come dumpsys meminfo, Perfetto o Android Studio Profiler.

Triage della memoria elevata in un'app ibrida

Quando diagnostichi una crescita inspiegabile della memoria durante le interazioni ricorrenti di WebView (ad esempio l'apertura di link web o la navigazione in feed basati sul web), utilizza il seguente workflow di triage per isolare se la perdita ha origine nel livello Java o nel motore nativo:

  1. Isola il tipo di perdita (Java o nativa): Esegui dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" prima e dopo le transizioni utente ripetute (ad esempio l'apertura e la chiusura di articoli web o lo scorrimento dei feed).

    • Osservazione: se i conteggi di Activities e WebViews rimangono stabili (ad esempio, 1-2 istanze attive), l'app non sta perdendo i Activity contesti o le istanze Java WebView.
  2. Misura il delta di memoria tra le interazioni (monitoraggio delle serie temporali): acquisisci snapshot di dumpsys meminfo in più interazioni utente per calcolare la frequenza di allocazione per transizione:

    • Osservazione: l'heap Java rimane limitato e integro (con picchi durante l'utilizzo e diminuisce dopo il garbage collection), ma Private Other e Native Heap aumentano costantemente di diversi megabyte per transizione. Ciò dimostra che la perdita si verifica interamente nella memoria nativa al di fuori del runtime ART. I dump dell'heap Java standard (.hprof) non mostreranno alcun problema.
  3. Esamina le mappe della memoria anonima: esamina le mappe della memoria di processo utilizzando ADB (vedi Esaminare le mappe e le allocazioni di memoria):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Osservazione: la crescita della memoria è concentrata in [anon:partition_alloc] o negli heap del motore di scripting incorporato, accompagnata da un lento aumento dei riferimenti globali JNI. Ciò indica che, sebbene le visualizzazioni Java siano state sostituite, gli oggetti pagina nativi sottostanti o i binding JavaScript non sono stati rilasciati.
  4. Correzione:

    • Assicurati che ogni WebView riciclato o eliminato interrompa esplicitamente gli script attivi (stopLoading()), cancelli la cronologia e chiami destroy().
    • Chiudi i callback del bridge JavaScript personalizzati o i riferimenti globali JNI associati alle visualizzazioni chiuse.
    • Verifica che Private Other e l'RSS del processo si stabilizzino dopo le transizioni di navigazione.

Risorse aggiuntive

Per saperne di più sul debug e sulla profilazione della memoria e del rendimento di WebView, consulta le seguenti risorse: