Gestire e diagnosticare la memoria di WebView

WebView esegue codice nativo in più processi per il rendering dei contenuti web nella tua app per Android. Se lasci le istanze WebView non gestite, possono verificarsi perdite di memoria, arresti anomali dovuti a esaurimento della memoria (OOM) e un peggioramento delle prestazioni dell'app.

Questo documento spiega il modello di memoria multiprocesso di WebView, descrive come gestire correttamente il suo ciclo di vita per evitare perdite e fornisce flussi di lavoro pratici per diagnosticare i problemi di memoria.

Comprendere l'architettura della memoria di WebView

Per gestire in modo efficace la memoria WebView, scopri 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 viene eseguito il codice Activity e Java o Kotlin.
    • Processo di rendering isolato:un processo sandbox separato (SandboxedProcessService) che analizza HTML e CSS, esegue JavaScript e visualizza le pagine web.
  • Impronta della memoria nativa: la maggior parte della memoria WebView, inclusi la grafica renderizzata, l'albero DOM e la memoria di runtime 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 sul sistema della memoria nativa:a differenza delle allocazioni dell'heap Java, che sono limitate dal limite maxHeap dell'app e hanno esito negativo rapidamente con un OutOfMemoryError, la memoria nativa può aumentare silenziosamente fino a raggiungere i gigabyte. Quando 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 memoria. In questo modo, il multitasking complessivo del dispositivo viene degradato prima di chiudere l'app in primo piano.

Gestire il ciclo di vita di WebView

Una corretta gestione del ciclo di vita è fondamentale per prevenire perdite di memoria. Un errore comune è presumere che la rimozione di un WebView dal layout o il completamento automatico di un Activity liberi la memoria.

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

Liberare spazio dalle istanze WebView

Per garantire un arresto pulito e il rilascio delle risorse quando Activity o Fragment viene distrutto:

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

Il seguente esempio mostra come liberare spazio correttamente in 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-distruzione

Quando chiami destroy(), il sistema rilascia il contesto Activity, pulisce le gerarchie delle visualizzazioni e interrompe il lavoro in background del web. Tuttavia, potresti notare che la memoria fisica del processo (Resident Set Size) non scende immediatamente al valore di base 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 Activity cumulative quando gli utenti navigano all'interno e all'esterno delle schermate basate sul web.

Metriche di debug principali

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

  • Dimensioni del set residente (RSS): la RAM fisica totale mappata nel processo, incluso il codice e le librerie condivisi (etichettati come Totale in Android Studio Profiler).

  • RSS anonimo (RssAnon): memoria allocata direttamente dal processo che non è supportata da un file sul disco (ad esempio heap nativo e allocazioni del runtime JavaScript). Rappresenta il costo della memoria principale dei tuoi contenuti web (etichettato come Allocated in Android Studio Profiler).

  • Footprint della memoria privata (PMF): la somma di RSS anonimo e swap (zRAM). PMF riflette il carico di memoria non eliminabile effettivo imposto dalla tua app sul sistema.

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

  • Conteggi oggetti live (WebViews, Activities, Views): il numero di istanze attive di UI, Context e WebView mantenute in memoria. Il monitoraggio di questi identifica se l'aumento della memoria è causato da riferimenti Java conservati o allocazioni solo native.

  • Heap privato e nativo:in dumpsys meminfo, le allocazioni C/C++ native e i mapping di memoria personalizzati (come PartitionAlloc di Chromium o gli heap di runtime JavaScript incorporati) vengono visualizzati in Heap nativo e Altro privato anziché in Heap Java.

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

Flussi di lavoro 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:

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

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

Controllare i conteggi degli oggetti attivi

Per determinare se l'aumento della memoria è causato da oggetti del framework Java mantenuti (come i componenti UI) o da allocazioni native, esamina la sezione Objects 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 di oggetti framework attivi, handle IPC e allocazioni di pacchetti. Per la diagnostica di WebView, concentrati principalmente su Activities e WebViews.

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

  • Perdita di istanze:se WebViews o Activities aumentano a ogni navigazione e non tornano alla base di riferimento, la tua app perde l'istanza Java WebView o l'host Activity (ad esempio, a causa di un ViewGroup.removeView() mancante o di riferimenti a listener conservati). Poiché un leak Activity blocca l'intero albero di visualizzazione e le risorse immagine decodificate in memoria, le visite ripetute esauriranno rapidamente l'heap Java e causeranno arresti anomali di OutOfMemoryError.

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

Profila il processo di rendering isolato utilizzando la CLI

L'esecuzione di dumpsys meminfo con il nome pacchetto dell'app restituisce solo la memoria per il processo host principale. Per esaminare il processo del renderer 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. Ispeziona 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 della memoria

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

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

La tabella seguente elenca i tag di memoria anonimi comuni e la loro pertinenza per l'aumento della memoria:

Tag di memoria OOB Pertinenza con i contenuti web e app Qual è la 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ì (alto): il caricamento di pagine web pesanti, DOM ricchi di contenuti multimediali o la mancata chiamata di destroy() su istanze WebView eliminate gonfiano direttamente questo tag.
[anon:scudo...] o [anon:libc_malloc] Allocatori heap nativi di Android (Scudo / jemalloc) Allocazioni native C/C++ generali utilizzate dalle librerie NDK, dai ponti JNI e dalle pipeline grafiche native. Sì (da moderato ad alto): la crescita si verifica quando i wrapper JNI nativi o le dipendenze C++ di terze parti conservano allocazioni non rilasciate durante le navigazioni.
[anon:...] (ad esempio, [anon:quickjs_heap...]) Script personalizzati o runtime nativi Motori JavaScript incorporati, runtime WebAssembly personalizzati o pool di buffer nativi personalizzati. Sì (in base al contesto): comune nelle app ibride che eseguono motori di scripting insieme a visualizzazioni native e non riescono a liberare spazio dai binding di runtime.

Limitazioni delle API di memoria in-app

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

Prioritizzazione dell'utilizzo elevato di memoria in un'app ibrida

Quando diagnostichi una crescita inspiegabile della memoria durante le interazioni WebView ricorrenti (ad esempio l'apertura di link web o la navigazione in feed basati sul web), utilizza il seguente flusso di lavoro 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 ripetute degli utenti (ad esempio l'apertura e la chiusura di articoli web o lo scorrimento dei feed).

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

    • Osservazione:l'heap Java rimane limitato e integro (con picchi durante l'utilizzo e cali dopo la garbage collection), ma Altro privato e Heap nativo 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 di memoria anonime:esamina le mappe di memoria del processo utilizzando ADB (vedi Esamina le mappe di memoria e le allocazioni):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Osservazione: la crescita della memoria è concentrata in [anon:partition_alloc] o in 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().
    • Elimina i callback del bridge JavaScript personalizzato o i riferimenti globali JNI associati alle visualizzazioni chiuse.
    • Conferma che Private Other e l'elaborazione RSS si stabilizzino dopo le transizioni di navigazione.

Risorse aggiuntive

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