Administra y diagnostica la memoria de WebView

WebView ejecuta código nativo en varios procesos para renderizar contenido web en tu app para Android. Si dejas las instancias de WebView sin administrar, se pueden producir 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 la memoria de WebView de manera eficaz, 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 vuelva a un solo proceso):

    • Proceso de 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 aislado independiente (SandboxedProcessService) que analiza HTML y CSS, ejecuta JavaScript y renderiza páginas web.
  • Huella de memoria nativa: La mayor parte de la memoria de WebView, incluidos los gráficos renderizados, el árbol DOM y la memoria de tiempo 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 contenedor de Java ligero y no captura la memoria real que usa el contenido web.

  • Impacto del sistema de la memoria nativa: A diferencia de las asignaciones de montón de Java, que están limitadas por el límite maxHeap de la app y fallan rápidamente con un OutOfMemoryError, la memoria nativa puede crecer silenciosamente a 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 multitarea general del dispositivo antes de finalizar la app en primer plano.

Administra el ciclo de vida de WebView

La administración adecuada del ciclo de vida es fundamental para evitar fugas de memoria. Un error común es suponer que quitar un WebView de tu diseño o permitir que un Activity finalice automáticamente libera 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 desmantelamiento en el ciclo de vida de tu componente host (como onDestroy()), detener la ejecución de la página activa, separar la vista de su contenedor y liberar las vinculaciones nativas.

Limpia las instancias de WebView

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

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

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

Comprende la memoria posterior a la destrucción

Cuando llamas a destroy(), el sistema libera el contexto Activity, limpia las jerarquías de vistas y detiene el trabajo en segundo plano 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 línea de base anterior a WebView.

Este comportamiento es normal. Las cachés de tiempo de ejecución nativas, las bibliotecas compartidas y las páginas de memoria asignadas permanecen residentes en el proceso hasta que el sistema operativo las recupera o el proceso finaliza. El objetivo principal de destroy() es evitar fugas de memoria acumulativas de Activity cuando los usuarios navegan dentro y fuera de las pantallas con tecnología 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 RAM física total asignada al proceso, incluido 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 de tiempo de ejecución de JavaScript). Esto representa el costo de memoria principal de tu contenido web (etiquetado como Asignado en el Generador de perfiles de Android Studio).

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

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

  • Recuentos de objetos activos (WebViews, Activities, Views): Es la cantidad de instancias activas de IU, Context y WebView que se almacenan en la memoria. El seguimiento de estos elementos identifica si el crecimiento de la memoria se debe a referencias de Java retenidas o asignaciones solo nativas.

  • Otro privado y montón nativo: En dumpsys meminfo, las asignaciones nativas de C/C++ y las asignaciones de memoria personalizadas (como PartitionAlloc de Chromium o montones de tiempo de ejecución de JavaScript incorporados) aparecen en Montón nativo y Otro privado en lugar de Montón de Java.

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

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 diagnóstico y generación de perfiles

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

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

  • Seguimiento de memoria con Perfetto: Usa Perfetto para registrar contadores de memoria a nivel del sistema (como RSS y RSS anónimo) para 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 de montón de Perfetto. Usa las Herramientas para desarrolladores de Chrome para inspeccionar instantáneas de montón de JavaScript y asignaciones de DOM dentro del contenido web.

Inspecciona los recuentos de objetos activos

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

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

El resultado muestra 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, controladores de IPC y 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 aumentan en cada navegación y no vuelven a la línea de base, tu app está filtrando la instancia de Java WebView o el host Activity (por ejemplo, debido a un ViewGroup.removeView() faltante o referencias de objeto de escucha retenidas). Debido a que un Activity filtrado 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 causarán fallas de OutOfMemoryError.

  • Fuga nativa o de DOM: Si WebViews y Activities permanecen constantes mientras el RSS total del proceso y Otro privado continúan aumentando, la fuga se origina en recursos nativos no liberados, elementos DOM o vinculaciones del motor de JavaScript. Debido a que estas asignaciones residen en la memoria nativa y omiten el recolector de elementos no utilizados 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 perfiles del proceso de renderizador aislado con la CLI

Si ejecutas dumpsys meminfo con el nombre del paquete de tu app, solo se generará la memoria del proceso host principal. Para inspeccionar el proceso de renderizador 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"

    El resultado 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 de renderizador con su PID:

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

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

Inspecciona los mapas de memoria y las asignaciones

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

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

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

Etiqueta de memoria Subsistema Relevancia para el contenido web y de la app ¿Causa común de aumento de memoria?
[anon:partition_alloc] PartitionAlloc de Chromium Asignaciones para árboles DOM, renderización de búferes, montón de JavaScript V8 y ejecución de WebAssembly en WebView. Sí (alto): Cargar páginas web pesadas, DOMs enriquecidos con contenido multimedia o no llamar a destroy() en instancias descartadas de WebView infla 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 de NDK, los puentes JNI y las canalizaciones de gráficos nativas. Sí (de moderado a alto): El crecimiento se produce cuando los contenedores nativos de JNI 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 tiempos de ejecución nativos Motores de JavaScript incorporados, tiempos 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 de tiempo de ejecución.

Limitaciones de las APIs de memoria en la app

Las APIs de memoria en la app (como Debug.getMemoryInfo o ActivityManager.getProcessMemoryInfo) solo miden el proceso de llamada. En el modo de varios procesos, 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 la memoria alta en una app híbrida

Cuando diagnostiques un crecimiento de memoria inexplicable durante las interacciones recurrentes de WebView (como abrir vínculos web o navegar por feeds con tecnología 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 en comparación con nativo): 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 permanecen estables (por ejemplo, de 1 a 2 instancias activas), la app no está filtrando Activity contextos ni instancias de Java WebView.
  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 permanece limitado y en buen estado (aumenta durante el uso y disminuye después de la recolección de elementos no utilizados), pero Otro privado y Montón nativo aumentan de manera constante en varios megabytes por transición. Esto demuestra que la fuga está completamente en la memoria nativa fuera del tiempo 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 de 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 [anon:partition_alloc] o en montones de motores de secuencias de comandos incorporados, 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().
    • Desmantela 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: