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 no se administran las instancias de WebView, se pueden producir fugas de memoria, fallas por falta de memoria (OOM) y un rendimiento deficiente 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 WebView de manera eficaz, debes comprender 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 código Activity y 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.
  • Espacio en memoria nativa: La mayor parte de la WebView memoria, incluida la memoria de gráficos renderizados, el árbol del DOM y el entorno de ejecución de JavaScript, se asigna en la memoria nativa, no en el montón de Java. Un volcado del montón de Java (.hprof) solo muestra un objeto wrapper de Java ligero y no captura la memoria real que usa el contenido web.

  • Impacto en el sistema de la memoria nativa: A diferencia de las asignaciones de la pila 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 multitarea general del dispositivo antes de cerrar 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 del diseño o dejar 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 cierre en el ciclo de vida del componente host (como onDestroy()), detener la ejecución activa de la página, 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 principal (ViewGroup).
  2. Detiene la carga activa y borra el historial de navegación.
  3. Llamar 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();
}

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 valor de referencia previo a WebView.

Este comportamiento es normal. Las memorias caché del tiempo 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 pérdidas de memoria acumulativas de Activity cuando los usuarios navegan dentro y fuera de las pantallas potenciadas por 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 RAM física total asignada al proceso, incluido el código y las bibliotecas compartidos (etiquetados como Total en el Android Studio Profiler).

  • RSS anónimo (RssAnon): Es la memoria asignada directamente por el proceso que no está respaldada por un archivo en el disco (como las asignaciones de montón nativo y de tiempo de ejecución de JavaScript). Representa el costo de memoria principal de tu contenido web (etiquetado como Asignado 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). El PMF refleja la carga de memoria real no expulsable que tu app impone en el sistema.

  • PMF del navegador en comparación con el PMF del renderizador: 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 picos principalmente en el proceso de renderizador.

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

  • Montón nativo y otro privado: En dumpsys meminfo, las asignaciones nativas de C/C++ y las asignaciones de memoria personalizadas (como los montones de tiempo de ejecución de JavaScript integrados o de Chromium PartitionAlloc) 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 procesos y sus categorías, consulta el glosario de memoria de procesos.

Flujos de trabajo de diagnóstico prácticos

Dado 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 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 de 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.

Cómo inspeccionar los recuentos de objetos activos

Para determinar si el crecimiento de la memoria se debe a objetos retenidos del framework de Java (como componentes de 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"

El resultado muestra los recuentos de objetos publicados actualmente:

 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, identificadores 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) varias veces y compara los recuentos:

  • Fuga de instancias: Si WebViews o Activities aumentan en cada navegación y no vuelven al valor de referencia, tu app tiene una fuga de la instancia WebView de Java o del host Activity (por ejemplo, debido a un ViewGroup.removeView() faltante o a referencias de listeners retenidas). Debido a que un Activity con fugas fija todo su árbol de vistas y los recursos de imágenes decodificadas en la memoria, las visitas repetidas agotarán rápidamente el montón de Java y provocarán fallas de OutOfMemoryError.

  • Fuga de DOM o nativa: 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 lanzados, elementos DOM o vinculaciones del motor de JavaScript. Dado que estas asignaciones residen en la memoria nativa y omiten el recolector de basura de ART, siguen siendo invisibles para las herramientas estándar de detección de fugas de Java y se siguen acumulando hasta que el sistema operativo finaliza la app.

Cómo crear 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 de 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"

    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 la 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 cliente del navegador:

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

Cómo inspeccionar 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 aplicación y la Web ¿Causa común del aumento de memoria?
[anon:partition_alloc] Chromium PartitionAlloc Asignaciones para árboles DOM, búferes de procesamiento, montón de JavaScript de V8 y ejecución de WebAssembly en WebView. Sí (alta): Cargar páginas web pesadas, DOM con muchos elementos multimedia o no llamar a destroy() en instancias de WebView descartadas infla directamente esta etiqueta.
[anon:scudo...] o [anon:libc_malloc] Asignadores de montón nativos de Android (Scudo / jemalloc) Son las asignaciones nativas generales de C/C++ que usan las bibliotecas del NDK, los puentes JNI y las canalizaciones de gráficos nativos. Sí (de moderado a alto): El crecimiento se produce cuando los wrappers de JNI nativos o las dependencias de C++ de terceros conservan 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 integrados, tiempos de ejecución personalizados de WebAssembly o grupos de búferes nativos personalizados Sí (depende del contexto): Es común en las apps híbridas que ejecutan motores de scripting junto con vistas nativas y no limpian las vinculaciones del tiempo de ejecución.

Limitaciones de las APIs de memoria integrada 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 priorizar el uso elevado de memoria en una app híbrida

Cuando diagnostiques un crecimiento inexplicable de la memoria durante las 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 pérdida se origina en la capa de Java o en el motor nativo:

  1. Aísla el tipo de fuga (Java frente a 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, de 1 a 2 instancias activas), la app no tiene fugas de contextos de Activity ni de instancias de WebView de Java.
  2. Medir 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 heap de Java se mantiene limitado y en buen estado (aumenta durante el uso y disminuye después de la recolección de elementos no utilizados), pero Private Other y Native Heap aumentan de forma constante en varios megabytes por transición. Esto demuestra que la fuga se produce por completo en la memoria nativa fuera del tiempo de ejecución de ART. Los volcados de pila de Java estándar (.hprof) no mostrarán ningún problema.
  3. Inspecciona mapas de memoria anónimos: Examina los mapas de memoria del proceso con ADB (consulta Inspecciona mapas y asignaciones de memoria):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Observación: El crecimiento de la memoria se concentra en las pilas de [anon:partition_alloc] o del motor de secuencias de comandos integrado, 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 explícitamente los scripts activos (stopLoading()), borre el historial y llame a destroy().
    • Desarma las devoluciones de llamada del puente de JavaScript personalizado o las referencias globales de JNI asociadas con las vistas descartadas.
    • Confirma que Private Other y el proceso RSS 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 la memoria y el rendimiento de WebView, consulta los siguientes recursos: