WebView y memoria

WebView es un componente potente que te permite mostrar contenido web en tu aplicación para Android. Sin embargo, debido a que es esencialmente un motor de navegador con todas las funciones (Chromium), tiene un espacio en memoria significativo y una arquitectura compleja de varios procesos.

Información técnica: Arquitectura de varios procesos

En los dispositivos modernos con tecnología Android, WebView usa un modelo de varios procesos para mejorar la seguridad y la estabilidad. Cuando tu app usa un WebView, la memoria se distribuye en diferentes procesos:

  1. Proceso del navegador (el proceso de la app): Es el proceso principal de tu aplicación. Contiene el objeto WebView de Java y la parte del "navegador" del motor de Chromium. Este proceso administra la IU, las solicitudes de red y la renderización de la GPU (integrada directamente con la canalización de renderización de HWUI de Android). A diferencia de Chrome, WebView no tiene un proceso de GPU independiente.
  2. Proceso de renderizador: Este proceso es responsable de analizar HTML, ejecutar JavaScript y el diseño. Está aislado del resto del sistema por motivos de seguridad. Actualmente, las apps solo obtienen un proceso de renderizador para todos los WebViews (excepto en algunos casos especiales poco frecuentes), a diferencia de Chrome, que suele usar procesos de renderizador independientes para diferentes sitios.

Arquitectura de WebView

Por qué es importante para la memoria

Cuando usas dumpsys meminfo <your_package>, solo ves la memoria que usa el proceso del navegador (el proceso de tu app). La memoria que usa el proceso del renderizador se contabiliza por separado.

Dentro del proceso del navegador, la memoria de WebView se distribuye de la siguiente manera:

  • Montón de Java: Contiene el wrapper de WebView de Java y los objetos relacionados.
  • Montón nativo: Contiene las estructuras de datos internas, las memorias caché y el estado del motor del navegador Chromium. Ten en cuenta que, debido al uso de PartitionAlloc, es posible que algunas asignaciones nativas de WebView no se cuenten en "Native Heap" en dumpsys meminfo y, en su lugar, aparezcan en "Other" o "Unknown".
  • Memoria compartida: Se usa para compartir búferes gráficos y otros datos. Es posible que dumpsys meminfo no la clasifique claramente.

Herramientas de solución de problemas

Herramientas para desarrolladores de Chrome

La herramienta más potente para analizar la memoria dentro de WebView (el proceso de renderizador) son las Herramientas para desarrolladores de Chrome.

  1. Habilita la depuración de WebView en tu 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. Conecta el dispositivo con un cable USB.

  3. Abre Chrome en tu máquina anfitrión y navega a chrome://inspect/#devices.

  4. Busca tu app y haz clic en inspect.

  5. En la ventana de Herramientas para desarrolladores, ve a la pestaña Memory para tomar instantáneas del montón o registrar líneas de tiempo de asignación para el montón de JavaScript.

dumpsys meminfo

Usa adb shell dumpsys meminfo --all <package> para ver un desglose de la memoria. Busca la categoría WebView en el resultado y los recuentos de objetos.

Genera el perfil del renderizador

Dado que el renderizador se ejecuta en un proceso independiente, no puedes generar el perfil de su montón nativo solo generando el perfil de tu app. Debes identificar el PID del proceso de renderizador de forma específica.

Para identificar el PID del renderizador correcto cuando hay varios WebViews activos, haz lo siguiente:

  1. Usa dumpsys activity:

    adb shell dumpsys activity processes <your_package_name>
    

    Busca la sección mConnections. Verás un ConnectionRecord que vincula tu app a un SandboxedProcessService. El PID de ese proceso es tu renderizador. Ejemplo:

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. Verifica los nombres de los procesos: Los procesos de renderizador suelen llamarse com.google.android.webview:sandboxed_processX o similar. Si solo una app usa un WebView, es probable que solo haya uno.

Una vez que tengas el PID, puedes generar su perfil con heapprofd.

Prácticas recomendadas para la memoria de WebView

Destrucción explícita

Se espera que las apps llamen a WebView.destroy() para indicar cuándo terminaron con una instancia.

Si bien WebView intenta garantizar que las instancias se puedan recolectar como elementos no utilizados y liberar todos sus recursos automáticamente, es difícil garantizar esto en el 100% de los casos. Incluso cuando funciona la recolección automática de elementos no utilizados, puede demorarse significativamente, lo que hace que la app conserve los recursos mucho más tiempo de lo esperado.

Si una app llama a WebView.destroy() en el momento adecuado (p.ej., en Activity.onDestroy()), conservar una referencia al objeto WebView en sí no filtrará ningún recurso nativo significativo. No es estrictamente necesario anular las referencias al objeto WebView en los campos de actividad después de destruirlo, ya que se limpiará cuando la actividad en sí se recolecte como elemento no utilizado.

Ejercicios: Experiencia práctica con la memoria de WebView

Ejercicio 1: Observa la huella de varios procesos

  1. Inicia MemoryLab y toma una medición de referencia de la memoria de tu app:

    adb shell dumpsys meminfo com.android.memorylab
    

    Referencia de muestra (rango): TOTAL PSS: 18915 KB

  2. Presiona Launch WebView (Normal).

  3. En WebView, presiona Allocate JS Memory (1000 DIVs) varias veces.

  4. Vuelve a verificar la memoria de la app:

    adb shell dumpsys meminfo com.android.memorylab
    
  5. Observa que la memoria en el proceso de tu app no aumenta significativamente en comparación con la referencia. Esto se debe a que los elementos DOM están en el proceso de renderizador.

  6. Busca el proceso de renderizador:

    adb shell ps -A | grep webview | grep sandboxed
    

    Resultado de ejemplo:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. Verifica la memoria del proceso de renderizador (con su PID):

    adb shell dumpsys meminfo 14227
    
  8. Observa el PSS TOTAL alto del proceso de renderizador. En nuestra ejecución de muestra, saltó a ~55 MB después de algunas asignaciones. Ten en cuenta que las asignaciones de JavaScript (que controla el motor V8) suelen contribuir a las secciones Private Other o Unknown (mmap) de dumpsys meminfo, en lugar del montón de Dalvik.

Ejercicio 2: La fuga de WebView del lado de Java

Un error común es conservar una instancia de WebView en un campo estático o en un objeto de larga duración que se filtra. Debido a que el objeto WebView es un "ancla" pesada que conserva recursos nativos y, potencialmente, procesos de renderizador completos, filtrarlo es muy costoso.

Impacto de la fuga de WebView

  1. En MemoryLab, presiona Launch WebView (Java Leak).
  2. La actividad se cerrará automáticamente después de que se cargue la página (simulando la navegación repetida y la acumulación de fugas).
  3. Presiona el botón 4 veces.
  4. Verifica la cantidad de instancias de WebView en tu app:

    adb shell dumpsys meminfo com.android.memorylab
    

    Busca la sección Objects en la parte inferior. Verás que el recuento de WebViews aumentó a 4.

    Resultado de muestra (4 instancias filtradas) en 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. Captura un volcado de montón y usa AHAT para encontrar la fuga. Si no tienes ahat en tu ruta de acceso, puedes compilarlo desde el árbol de 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. En la interfaz web de AHAT (localhost:8888), haz clic en el vínculo allocations (o sites) en el menú superior para ver el uso de memoria general.

    Asignaciones de AHAT

  7. Busca la clase android.webkit.WebView. Haz clic en su recuento de instancias para ver todas las instancias activas. Deberías ver varias instancias en la lista.

    Instancias de WebView de AHAT

  8. Haz clic en una de las instancias de WebView filtradas. Desplázate hacia abajo hasta la sección Sample Path from GC Root. Verás que la mantiene la lista sLeakedWebViews en com.android.memorylab.WebViewActivity.

    Ruta de AHAT hacia la raíz de GC


← Nativo | ↑ Arriba | Código de la app →