WebView e memoria

WebView è un componente potente che ti consente di visualizzare contenuti web all'interno della tua app per Android. Tuttavia, poiché è essenzialmente un motore del browser completo (Chromium), ha un footprint della memoria significativo e un'architettura multi-processo complessa.

Background tecnico: architettura multi-processo

Sui dispositivi moderni con Android, WebView utilizza un modello multi-processo per migliorare la sicurezza e la stabilità. Quando la tua app utilizza una WebView, la memoria viene distribuita tra diversi processi:

  1. Processo del browser (il processo dell'app): questo è il processo principale della tua applicazione. Contiene l'oggetto Java WebView e la parte "browser" del motore Chromium. Questo processo gestisce l'interfaccia utente, le richieste di rete e il rendering della GPU (integrato direttamente con la pipeline di rendering HWUI di Android). A differenza di Chrome, WebView non ha un processo GPU separato.
  2. Processo di rendering: questo processo è responsabile dell'analisi dell'HTML, dell'esecuzione di JavaScript e del layout. È isolato dal resto del sistema per motivi di sicurezza. Attualmente, le app ottengono un solo processo di rendering per tutte le WebView (tranne in alcuni rari casi speciali), a differenza di Chrome che spesso utilizza processi di rendering separati per siti diversi.

Architettura di WebView

Perché è importante per la memoria

Quando utilizzi dumpsys meminfo <your_package>, vedi solo la memoria utilizzata da il processo del browser (il processo dell'app). La memoria utilizzata dal processo di rendering viene contabilizzata separatamente.

All'interno del processo del browser, la memoria di WebView viene distribuita come segue:

  • Heap Java: contiene il wrapper Java WebView e gli oggetti correlati.
  • Heap nativo: contiene le strutture di dati interne, le cache e lo stato del motore del browser Chromium. Tieni presente che, a causa dell'utilizzo di PartitionAlloc, alcune allocazioni native di WebView potrebbero non essere conteggiate in "Native Heap" in dumpsys meminfo e potrebbero invece essere visualizzate in "Other" o "Unknown".
  • Memoria condivisa: utilizzata per la condivisione di buffer grafici e altri dati. Questa potrebbe non essere classificata chiaramente da dumpsys meminfo.

Strumenti per la risoluzione dei problemi

Chrome DevTools

Lo strumento più potente per analizzare la memoria all'interno di WebView (il processo di rendering) è Chrome DevTools.

  1. Attiva il debug di WebView nella tua app:

    // NOTE: In production, this should be gated behind a developer setting
    // or only enabled for debuggable builds to prevent reverse engineering.
    WebView.setWebContentsDebuggingEnabled(true);
    
  2. Collega il dispositivo tramite USB.

  3. Apri Chrome sulla macchina host e vai a chrome://inspect/#devices.

  4. Trova la tua app e fai clic su Inspect.

  5. Nella finestra DevTools, vai alla scheda Memory per acquisire snapshot dell'heap o registrare le sequenze temporali di allocazione per l'heap JavaScript.

dumpsys meminfo

Utilizza adb shell dumpsys meminfo --all <package> per visualizzare una suddivisione della memoria. Cerca la categoria WebView nell'output e i conteggi degli oggetti.

Profilazione del renderer

Poiché il renderer viene eseguito in un processo separato, non puoi profilare il suo heap nativo semplicemente profilando la tua app. Devi identificare in modo specifico il PID del processo di rendering.

Per identificare il PID del renderer corretto quando sono attive più WebView:

  1. Utilizza dumpsys activity:

    adb shell dumpsys activity processes <your_package_name>
    

    Cerca la sezione mConnections. Vedrai un ConnectionRecord che collega la tua app a un SandboxedProcessService. Il PID di questo processo è il tuo renderer. Esempio:

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. Controlla i nomi dei processi: i processi di rendering in genere hanno il nome com.google.android.webview:sandboxed_processX o simile. Se una sola app utilizza una WebView, probabilmente ne sarà presente una sola.

Una volta ottenuto il PID, puoi profilarlo utilizzando heapprofd.

Best practice per la memoria di WebView

Distruzione esplicita

Le app devono chiamare WebView.destroy() per indicare quando hanno terminato di utilizzare un'istanza.

Sebbene WebView tenti di garantire che le istanze possano essere sottoposte a Garbage Collection e rilasciare automaticamente tutte le risorse, è difficile garantirlo nel 100% dei casi. Anche quando la Garbage Collection automatica funziona, potrebbe essere ritardata in modo significativo, facendo sì che l'app mantenga le risorse molto più a lungo del previsto.

Se un'app chiama WebView.destroy() al momento opportuno (ad es. in Activity.onDestroy()), mantenere un riferimento all'oggetto WebView stesso non comporterà la perdita di risorse native significative. Non è strettamente necessario impostare su null i riferimenti all'oggetto WebView nei campi Activity dopo averlo eliminato, poiché verrà pulito quando l'Activity stessa verrà sottoposta a Garbage Collection.

Esercizi: pratica con la memoria di WebView

Esercizio 1: osservare il footprint multi-processo

  1. Avvia MemoryLab ed esegui una misurazione di base della memoria della tua app:

    adb shell dumpsys meminfo com.android.memorylab
    

    Base di riferimento di esempio (rango): TOTAL PSS: 18915 KB

  2. Tocca Launch WebView (Normal).

  3. In WebView, tocca Allocate JS Memory (1000 DIVs) più volte.

  4. Controlla di nuovo la memoria dell'app:

    adb shell dumpsys meminfo com.android.memorylab
    
  5. Nota che la memoria nel processo dell'app non aumenta in modo significativo rispetto alla base di riferimento. Questo perché gli elementi DOM si trovano nel processo di rendering.

  6. Trova il processo di rendering:

    adb shell ps -A | grep webview | grep sandboxed
    

    Output di esempio:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. Controlla la memoria del processo di rendering (utilizzando il relativo PID):

    adb shell dumpsys meminfo 14227
    
  8. Osserva l'elevato PSS TOTALE del processo di rendering. Nella nostra esecuzione di esempio, è aumentato a ~55 MB dopo alcune allocazioni. Tieni presente che le allocazioni JavaScript (gestite dal motore V8) in genere contribuiscono alle sezioni Private Other o Unknown (mmap) di dumpsys meminfo, anziché all'heap Dalvik.

Esercizio 2: perdita di WebView lato Java

Un errore comune è mantenere un'istanza WebView in un campo statico o in un oggetto di lunga durata che perde. Poiché l'oggetto WebView è un "ancoraggio" pesante che mantiene le risorse native e potenzialmente interi processi di rendering, la sua perdita è molto costosa.

Impatto della perdita di WebView

  1. In MemoryLab, tocca Launch WebView (Java Leak).
  2. L'attività si chiuderà automaticamente dopo il caricamento della pagina (simulando la navigazione ripetuta e l'accumulo di perdite).
  3. Tocca il pulsante 4 volte.
  4. Controlla il numero di istanze WebView nella tua app:

    adb shell dumpsys meminfo com.android.memorylab
    

    Cerca la sezione Objects in basso. Vedrai che il conteggio di WebViews è aumentato a 4.

    Output di esempio (4 istanze perse) su rango:

     Objects
               Views:       51         ViewRootImpl:        5
         AppContexts:       14           Activities:        5
              Assets:       38        AssetManagers:        0
       Local Binders:       55        Proxy Binders:       77
       Parcel memory:       41         Parcel count:       68
    Death Recipients:        3             WebViews:        4
    
  5. Acquisisci un dump dell'heap e utilizza AHAT per trovare la perdita. Se non hai ahat nel tuo percorso, puoi crearlo dall'albero Android:

    # Dump heap from device
    adb shell am dumpheap com.android.memorylab /data/local/tmp/heap.hprof
    adb pull /data/local/tmp/heap.hprof
    # Run ahat using the built JAR (found in out/host/linux-x86/framework/)
    java -jar out/host/linux-x86/framework/ahat.jar -p 8888 heap.hprof
    
  6. Nell'interfaccia web di AHAT (localhost:8888), fai clic sul link allocations (o sites) nel menu in alto per visualizzare la memoria utilizzata complessiva.

    Allocazioni AHAT

  7. Cerca la classe android.webkit.WebView. Fai clic sul conteggio delle istanze per visualizzare tutte le istanze attive. Nell'elenco dovresti vedere più istanze.

    Istanze WebView AHAT

  8. Fai clic su una delle istanze WebView perse. Scorri verso il basso fino alla sezione Sample Path from GC Root. Vedrai che viene mantenuta dall'elenco sLeakedWebViews in com.android.memorylab.WebViewActivity.

    AHAT Path to GC Root


← Nativo | ↑ Su | Codice dell'app →