Administra y diagnostica la memoria de WebView

WebView ejecuta código nativo en varios procesos para renderizar contenido web en tu app para Android. Dejar instancias de WebView sin administrar puede provocar fugas de memoria, fallas por falta de memoria (OOM) y un rendimiento degradado de la app.

En este documento, se explica el modelo de memoria de varios procesos de WebView, se describe cómo administrar correctamente su ciclo de vida para evitar fugas y se proporcionan flujos de trabajo prácticos para diagnosticar problemas de memoria.

Comprende la arquitectura de memoria de WebView

Para administrar de manera eficaz la memoria de WebView, comprende cómo Android asigna recursos para el contenido web:

  • Ejecución de varios procesos: En Android 8.0 (nivel de API 26) y versiones posteriores, WebView separa el contenido web de las funciones principales de tu app en varios procesos (en dispositivos con poca RAM, es posible que recurra a un solo proceso):

    • Proceso del host (navegador): Es el proceso principal de la app en el que se ejecutan tu Activity y el código Java o Kotlin.
    • Proceso de renderizador aislado: Es un proceso de zona de pruebas independiente (SandboxedProcessService) que analiza HTML y CSS, ejecuta JavaScript y renderiza páginas web.
  • Espacio en memoria nativa: La mayor parte de la memoria de WebView, incluidos los gráficos renderizados, el árbol del DOM y la memoria del entorno de ejecución de JavaScript, se asigna en la memoria nativa, no en el montón de Java. Un volcado de montón de Java (.hprof) solo muestra un objeto de wrapper de Java liviano y no captura la memoria real que usa el contenido web.

  • Impacto de la memoria nativa en el sistema: A diferencia de las asignaciones de montón de Java, que están limitadas por el límite de maxHeap de la app y fallan rápidamente con un OutOfMemoryError, la memoria nativa puede crecer de forma silenciosa hasta alcanzar gigabytes. A medida que la memoria nativa no liberada llena la RAM física y el espacio de intercambio (zRAM), el Low Memory Killer (LMK) de Android comienza a finalizar los procesos en segundo plano para recuperar memoria. Esto degrada la capacidad general de realizar varias tareas a la vez antes de que, finalmente, se cierre la app en primer plano.

Administra el ciclo de vida de WebView

La administración adecuada del ciclo de vida es fundamental para evitar las fugas de memoria. Un error común es suponer que quitar un WebView de tu diseño o permitir que un Activity finalice libera automáticamente su memoria.

Para garantizar la limpieza completa de las referencias de contexto de Java y los recursos de renderización nativos, debes organizar de forma explícita una secuencia de eliminación en el ciclo de vida de tu componente host (como onDestroy()), detener la ejecución de la página activa, desconectar la vista de su contenedor y liberar las vinculaciones nativas.

Cómo limpiar las instancias de WebView

Para garantizar un cierre limpio y liberar recursos cuando se destruya tu Activity o Fragment, haz lo siguiente:

  1. Quita el WebView de su contenedor superior (ViewGroup).
  2. Detener la carga activa y borrar el historial de navegación
  3. Llamar a destroy()
  4. Borra la referencia a null.

En el siguiente ejemplo, se muestra cómo limpiar correctamente 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();
}

Información sobre la memoria posterior a la destrucción

Cuando llamas a destroy(), el sistema libera el contexto de Activity, limpia las jerarquías de vistas y detiene el trabajo en segundo plano de la Web. Sin embargo, es posible que observes que la memoria física del proceso (tamaño del conjunto residente) no disminuye de inmediato a su modelo de referencia anterior a WebView.

Este comportamiento es normal. Las cachés del entorno de ejecución nativo, las bibliotecas compartidas y las páginas de memoria asignadas permanecen residentes en el proceso hasta que el sistema operativo las reclama o el proceso finaliza. El objetivo principal de destroy() es evitar las fugas de memoria acumulativas de Activity cuando los usuarios navegan dentro y fuera de las pantallas basadas en la Web.

Métricas clave de depuración

Cuando analices el consumo de memoria de WebView, enfócate en las siguientes métricas:

  • Tamaño del conjunto residente (RSS): Es la memoria RAM física total asignada al proceso, incluidos el código y las bibliotecas compartidos (etiquetados como Total en el Generador de perfiles de Android Studio).

  • RSS anónimo (RssAnon): Es la memoria asignada directamente por el proceso que no está respaldada por un archivo en el disco (como el montón nativo y las asignaciones del entorno de ejecución de JavaScript). Esto representa el costo de memoria principal de tu contenido web (etiquetado como Allocated en el Generador de perfiles de Android Studio).

  • Espacio en memoria privada (PMF): Es la suma del RSS anónimo y el espacio de intercambio (zRAM). La PMF refleja la carga de memoria real no expulsable que tu app impone al sistema.

  • PMF del navegador en comparación con el PMF del renderizador: Es la memoria que usa el proceso principal de tu app en comparación con la memoria que usa el proceso del renderizador aislado. El contenido web pesado provoca aumentos repentinos principalmente en el proceso del renderizador.

  • Recuentos de objetos activos (WebViews, Activities, Views): El número de instancias activas de UI, Context y WebView mantenidas en memoria. Si realizas un seguimiento de estos, podrás identificar si el crecimiento de la memoria se debe a referencias de Java retenidas o a asignaciones solo nativas.

  • Private Other y Native Heap: En dumpsys meminfo, las asignaciones nativas de C/C++ y las asignaciones de memoria personalizadas (como PartitionAlloc de Chromium o montones de entornos de ejecución de JavaScript incorporados) aparecen en Native Heap y Private Other, en lugar de Java Heap.

Para obtener más información sobre los contadores de memoria de procesos y sus categorías, consulta el Glosario de memoria de procesos.

Flujos de trabajo de diagnóstico prácticos

Debido a que WebView opera en varios procesos y asigna memoria nativa, usa las siguientes herramientas y técnicas para inspeccionar su huella:

Herramientas de generación de perfiles y diagnóstico

Para inspeccionar las asignaciones de memoria y diagnosticar las fugas, usa las siguientes herramientas:

  • Generador de perfiles de memoria de Android Studio: Usa el Generador de perfiles de memoria para visualizar las asignaciones nativas, hacer un seguimiento de las categorías de memoria a lo largo del tiempo y detectar fugas de Activity en las transiciones de pantalla.

  • Seguimiento de la memoria con Perfetto: Usa Perfetto para registrar los contadores de memoria a nivel del sistema (como RSS y RSS anónimo) y observar el crecimiento general de la memoria. Ten en cuenta que las asignaciones del motor nativo de WebView no producen pilas de llamadas en la herramienta de generación de perfiles del montón de Perfetto. Usa las Herramientas para desarrolladores de Chrome para inspeccionar las instantáneas del montón de JavaScript y las asignaciones del DOM dentro del contenido web.

Inspecciona los recuentos de objetos activos

Para determinar si el aumento de memoria se debe a objetos retenidos del framework de Java (como componentes de la IU) o a asignaciones nativas, inspecciona la sección Objects de dumpsys meminfo:

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

En el resultado, se muestran los recuentos de objetos activos:

 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

En esta sección, se muestran los recuentos de objetos de framework activos, los controladores de IPC y las asignaciones de Parcel. Para el diagnóstico de WebView, enfócate principalmente en Activities y WebViews.

Realiza la interacción del usuario objetivo (como abrir y cerrar una pantalla web) de forma repetida y compara los recuentos:

  • Fuga de instancias: Si WebViews o Activities se incrementan en cada navegación y no vuelven al valor de referencia, tu app tiene una fuga de la instancia de Java WebView o del host Activity (por ejemplo, debido a que falta ViewGroup.removeView() o a referencias de objetos de escucha retenidas). Debido a que una Activity filtrada fija todo su árbol de vistas y recursos de imagen decodificados en la memoria, las visitas repetidas agotarán rápidamente el montón de Java y provocarán fallas de OutOfMemoryError.

  • Fuga nativa o del DOM: Si WebViews y Activities permanecen constantes mientras el RSS total del proceso y Private Other siguen aumentando, la fuga se origina en recursos nativos no liberados, elementos del DOM o vinculaciones del motor de JavaScript. Debido a que estas asignaciones residen en la memoria nativa y omiten el recolector de basura de ART, permanecen invisibles para las herramientas estándar de detección de fugas de Java y continúan acumulándose hasta que el sistema operativo finaliza la app.

Genera un perfil del proceso de renderizador aislado con la CLI

Si ejecutas dumpsys meminfo con el nombre del paquete de tu app, solo se mostrará la memoria del proceso del host principal. Para inspeccionar el proceso de renderización aislado en el que se renderizan las páginas web, haz lo siguiente:

  1. Busca el ID de proceso (PID) del servicio de renderizador aislado:

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

    En el resultado, se muestra el registro del proceso aislado y su PID RENDERER_PID (por ejemplo, 22155):

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Inspecciona el desglose de memoria del proceso del procesador con su PID:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. Inspecciona el proceso de la app host para evaluar la huella del lado del navegador:

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

Inspecciona mapas y asignaciones de memoria

Para ver qué subsistemas o asignadores nativos ocupan memoria anónima, inspecciona los mapas de memoria del proceso:

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

En la siguiente tabla, se enumeran las etiquetas de memoria anónima comunes y su relevancia para el crecimiento de la memoria:

Etiqueta de memoria Subsistema Relevancia para el contenido de la app y el contenido web ¿Cuál es una causa común del aumento de memoria?
[anon:partition_alloc] PartitionAlloc de Chromium Asignaciones para árboles del DOM, búferes de renderización, montón de JavaScript de V8 y ejecución de WebAssembly en WebView. Sí (alto): Cargar páginas web pesadas, DOM con mucho contenido multimedia o no llamar a destroy() en instancias de WebView descartadas aumenta directamente esta etiqueta.
[anon:scudo...] o [anon:libc_malloc] Asignadores de montón nativos de Android (Scudo / jemalloc) Asignaciones nativas generales de C/C++ que usan las bibliotecas del NDK, los puentes de JNI y las canalizaciones de gráficos nativas. Sí (moderado a alto): El crecimiento se produce cuando los wrappers de JNI nativos o las dependencias de C++ de terceros retienen asignaciones no liberadas en las navegaciones.
[anon:...] (por ejemplo, [anon:quickjs_heap...]) Secuencias de comandos personalizadas o entornos de ejecución nativos Motores de JavaScript incorporados, entornos de ejecución de WebAssembly personalizados o grupos de búferes nativos personalizados Sí (depende del contexto): Es común en las apps híbridas que ejecutan motores de secuencias de comandos junto con vistas nativas y no limpian las vinculaciones del entorno de ejecución.

Limitaciones de las APIs de memoria integrada en la app

Las APIs de memoria integrada en la app (como Debug.getMemoryInfo o ActivityManager.getProcessMemoryInfo) solo miden el proceso de llamada. En el modo multiproceso, estas APIs no pueden capturar la memoria que consume el proceso de renderizador aislado. Para obtener una evaluación precisa de la memoria total, usa herramientas del sistema como dumpsys meminfo, Perfetto o el Generador de perfiles de Android Studio.

Cómo clasificar el uso elevado de memoria en una app híbrida

Cuando diagnostiques un crecimiento de memoria inexplicable durante interacciones recurrentes de WebView (como abrir vínculos web o navegar por feeds basados en la Web), usa el siguiente flujo de trabajo de clasificación para aislar si la fuga se origina en la capa de Java o en el motor nativo:

  1. Aísla el tipo de fuga (Java o nativa): Ejecuta dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" antes y después de las transiciones repetidas del usuario (como abrir y cerrar artículos web o deslizar el dedo por los feeds).

    • Observación: Si los recuentos de Activities y WebViews se mantienen estables (por ejemplo, 1 o 2 instancias activas), la app no pierde contextos de Activity ni instancias de WebView de Java.
  2. Mide el delta de memoria en las interacciones (seguimiento de series temporales): Captura instantáneas de dumpsys meminfo en varias interacciones del usuario para calcular la tasa de asignación por transición:

    • Observación: El montón de Java se mantiene limitado y en buen estado (con aumentos durante el uso y disminuciones después de la recolección de elementos no utilizados), pero Private Other y Native Heap aumentan de forma constante varios megabytes por transición. Esto demuestra que la fuga se encuentra por completo en la memoria nativa fuera del entorno de ejecución de ART. Los volcados de montón de Java estándar (.hprof) no mostrarán ningún problema.
  3. Inspecciona los mapas de memoria anónimos: Examina los mapas de memoria del proceso con ADB (consulta Inspecciona los mapas de memoria y las asignaciones):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Observación: El crecimiento de la memoria se concentra en los montones de [anon:partition_alloc] o del motor de secuencias de comandos incorporado, acompañado de un aumento lento en las referencias globales de JNI. Esto indica que, si bien se reemplazaron las vistas de Java, no se liberaron los objetos de página nativos subyacentes ni las vinculaciones de JavaScript.
  4. Corrección:

    • Asegúrate de que cada WebView reciclado o descartado detenga de forma explícita las secuencias de comandos activas (stopLoading()), borre el historial y llame a destroy().
    • Anula las devoluciones de llamada de puente de JavaScript personalizadas o las referencias globales de JNI asociadas con las vistas descartadas.
    • Confirma que Private Other y el RSS del proceso se estabilicen después de las transiciones de navegación.

Recursos adicionales

Para obtener más información sobre la depuración y la generación de perfiles de memoria y el rendimiento de WebView, consulta los siguientes recursos: