Gestire e diagnosticare la memoria di WebView

WebView esegue il codice nativo su più processi per eseguire 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 all'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.

Comprendi l'architettura della memoria di WebView

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

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

    • Processo host (browser):il processo principale dell'app in cui vengono eseguiti il codice Activity e il codice Java o Kotlin.
    • Processo di rendering isolato: un processo in sandbox separato (SandboxedProcessService) che analizza HTML e CSS, esegue JavaScript ed esegue il rendering delle pagine web.
  • Footprint della memoria nativa:la maggior parte della memoria di WebView, inclusa la grafica di rendering, 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 della memoria nativa sul sistema:a differenza delle allocazioni heap Java, che sono limitate dal limite maxHeap dell'app e falliscono rapidamente con un OutOfMemoryError, la memoria nativa può crescere silenziosamente fino a gigabyte. Man mano che la memoria nativa non rilasciata riempie la RAM fisica e lo spazio di scambio (zRAM), il Low Memory Killer (LMK) di Android inizia a terminare i processi in background per recuperare memoria. Ciò riduce il multitasking complessivo del dispositivo prima di terminare l'app in primo piano.

Gestisci il ciclo di vita di WebView

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

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

Esegui la pulizia delle istanze WebView

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

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

L'esempio seguente mostra come eseguire correttamente la pulizia di 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 di oggetti View e interrompe il lavoro in background sul web. Tuttavia, potresti notare che la memoria fisica del processo (Resident Set Size) non scende immediatamente al livello di riferimento 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() è impedire perdite di memoria Activity cumulative quando gli utenti entrano ed escono dalle schermate basate sul web.

Metriche di debug principali

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).

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

  • Footprint della memoria privata (PMF): la somma di RSS anonimo e swap (zRAM). La PMF riflette l'effettivo carico di memoria non rimovibile 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 del renderer isolato. I contenuti web pesanti causano picchi principalmente nel processo del renderer.

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

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

Per saperne di più 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 ispezionare il suo footprint:

Strumenti di profilazione e diagnostica

Per ispezionare le allocazioni della memoria e diagnosticare i leak, 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 i leak di Activity nelle transizioni tra le schermate.

  • Monitoraggio della memoria con Perfetto:utilizza Perfetto per registrare i contatori della 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 stack di chiamate nello strumento di profilazione heap di Perfetto. Utilizza Chrome DevTools per ispezionare le istantanee heap JavaScript e le allocazioni DOM all'interno dei contenuti web.

Controlla i conteggi degli oggetti attivi

Per determinare se l'aumento della memoria è causato da oggetti del framework Java conservati (come i componenti dell'interfaccia utente) o da allocazioni native, controlla 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 il numero di oggetti del framework attivi, handle IPC e allocazioni Parcel. Per la diagnostica di WebView, concentrati principalmente su Activities e WebViews.

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

  • Leak dell'istanza:se WebViews o Activities aumenta a ogni navigazione e non torna al valore di riferimento, l'app sta perdendo l'istanza Java WebView o l'host Activity (ad esempio, a causa di un ViewGroup.removeView() mancante o di riferimenti ai listener conservati). Poiché un Activity con leak blocca l'intero albero delle visualizzazioni e le risorse delle immagini decodificate in memoria, le visite ripetute esauriranno rapidamente l'heap Java e causeranno arresti anomali di OutOfMemoryError.

  • Leak nativo o DOM:se WebViews e Activities rimangono costanti, mentre l'RSS totale del processo e Private Other continuano a salire, il leak ha origine in risorse native, elementi DOM o associazioni del motore JavaScript non rilasciati. Poiché queste allocazioni risiedono nella memoria nativa e bypassano il garbage collector ART, rimangono invisibili agli strumenti standard di rilevamento dei leak di Java e continuano ad accumularsi finché il sistema operativo non termina l'app.

Profila il processo di rendering isolato utilizzando l'interfaccia a riga di comando

L'esecuzione di dumpsys meminfo con il nome del pacchetto dell'app restituisce solo la memoria per il processo host principale. Per ispezionare il processo del renderer isolato in cui viene eseguito il rendering delle pagine web:

  1. Trova l'ID di 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 l'analisi della memoria del processo del renderer utilizzando il relativo PID:

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

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

Esamina le mappe e le allocazioni della memoria

Per vedere quali sottosistemi o allocatori nativi occupano memoria anonima, controlla 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 anonima comuni e la loro rilevanza per la crescita della memoria:

Tag di memoria OOB Pertinenza ai contenuti web e delle app 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 con molti contenuti multimediali o la mancata chiamata di destroy() su istanze WebView ignorate aumenta 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 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 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ì (a seconda del contesto): comune nelle app ibride che eseguono motori di scripting insieme a visualizzazioni native e non riescono a ripulire i 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à multiprocesso, queste API non possono acquisire la memoria utilizzata 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.

Risoluzione dei problemi di memoria elevata in un'app ibrida

Quando diagnostichi una crescita della memoria inspiegabile durante interazioni WebView ricorrenti (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 fuga di dati ha origine nel livello Java o nel motore nativo:

  1. Isola il tipo di leak (Java o nativo): 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 stabili (ad esempio, 1-2 istanze attive), l'app non presenta perdite di contesti Activity o istanze Java WebView.
  2. Misura il delta della memoria tra le interazioni (monitoraggio delle serie temporali): acquisisci istantanee 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 Private Other e Native Heap aumentano costantemente di diversi megabyte per transizione. Ciò dimostra che la perdita si trova 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 (consulta 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 negli heap [anon:partition_alloc] o 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 di pagina nativi o i binding JavaScript sottostanti 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 ignorate.
    • Verifica che Private Other e l'RSS del processo si stabilizzino dopo le transizioni di navigazione.

Risorse aggiuntive

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