Supervisa el uso de memoria

Para optimizar de manera eficaz el espacio en memoria de tu juego, primero debes comprender cómo la plataforma de Android mide la memoria y cómo usar la telemetría del sistema, las APIs de diagnóstico y las herramientas de generación de perfiles. En esta guía, se detalla cómo supervisar, capturar y analizar las asignaciones de memoria de tu juego según los nuevos lineamientos de la plataforma.

Cómo interpretar las métricas de RSS y de intercambio

Para analizar y depurar de manera eficaz el comportamiento de la memoria de tu juego, debes comprender las métricas técnicas exactas que usa la plataforma de Android para la aplicación de la memoria. Si deseas obtener información general detallada sobre cómo se procesa y supervisa este parámetro de telemetría en el entorno real, consulta la documentación de Android Vitals: Uso de memoria (RSS anónimo + intercambio).

1. RSS anónimo (RssAnon)

El tamaño del conjunto residente (RSS) mide la parte de la memoria ocupada por un proceso que se encuentra en la RAM física del dispositivo. El RSS se divide en memoria respaldada por archivos y memoria anónima. La métrica de aplicación de Android se centra estrictamente en el RSS anónimo:

  • Qué incluye: Páginas de memoria asignadas directamente por el proceso de tu juego que no están vinculadas a un archivo físico en el almacenamiento. Estas páginas incluyen montones de Java o Kotlin, pilas de ejecución de subprocesos y, lo que es más importante, asignaciones de memoria nativa (como asignadores de motor de C++ personalizados o bloques de memoria solicitados con malloc o new nativos y modificados por la lógica del juego). Obtén más información sobre esta métrica en el diccionario de memoria de proceso (RSS).
  • Por qué es importante: Los motores de juegos usan grandes grupos de memoria nativa para controlar la física, la renderización y la lógica. Debido a que estos grupos no están respaldados por archivos, residen por completo en el RSS anónimo y forman la mayor parte del espacio físico de tu juego.

2. Intercambio sin comprimir (VmSwap)

Android no admite un espacio de intercambio tradicional basado en disco debido al desgaste del almacenamiento flash y las restricciones de latencia. En su lugar, usa zRAM (intercambio sin comprimir):

  • Qué incluye: Cuando aumenta la presión de la RAM física, el daemon de administración de memoria del kernel comprime las páginas anónimas inactivas y las mueve a una parte dedicada y sin comprimir de la RAM física (zRAM).
  • El cálculo de la métrica: El sistema realiza un seguimiento de esto en función del tamaño sin comprimir (VmSwap) para evaluar la demanda real de memoria física del juego. Si tu juego asigna memoria y el sistema la intercambia a zRAM, aún se cuenta para el espacio total en memoria del juego.

3. Estados del proceso

El uso de memoria se desglosa por estados del proceso en Android Vitals. Para los desarrolladores de juegos, los SDKs o juegos de terceros también pueden activar servicios percibidos por el usuario o servicios en segundo plano de forma inesperada.

  • Qué incluye: Primer plano, servicios perceptibles, segundo plano y caché.
  • Por qué es importante: Los diferentes estados del proceso tienen diferentes impactos en la administración de memoria del SO Android. Es posible que no sepas que tu juego se ejecuta con un estado de proceso sensible si alguno de los SDKs de terceros activa una tarea en segundo plano de forma involuntaria. Supervisa si tu juego se ejecuta en segundo plano con RunningAppProcessInfo.

Interfaces de programación de aplicaciones (APIs)

Android proporciona APIs del sistema que permiten que tu juego responda de forma dinámica a la presión de la memoria y capture diagnósticos de memoria detallados en el tiempo de ejecución.

Cómo responder a eventos de reducción de memoria

El sistema usa onTrimMemory para notificar a tu app sobre los eventos del ciclo de vida que representan una buena oportunidad para que la app reduzca voluntariamente su uso de memoria y evite que el LMK (low-memory killer) la detenga para liberar memoria para que la usen otras apps.

Si el sistema detiene tu app en segundo plano, el usuario experimenta un inicio en frío lento cuando se reanuda. Reducir el uso de memoria en segundo plano ayuda a evitar estas finalizaciones en segundo plano.

Cuando respondas a eventos de reducción, libera asignaciones de memoria grandes y reconstruibles que no se necesiten de inmediato:

  • Ejemplo: Reduce o purga los mapas de bits almacenados en caché (decodificados del almacenamiento local) en respuesta a TRIM_MEMORY_UI_HIDDEN.

Kotlin

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
    override fun onTrimMemory(level: Int) {
        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }
        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Java

public class MainActivity extends AppCompatActivity implements ComponentCallbacks2 {
    public void onTrimMemory(int level) {
        switch (level) {
            if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
                // Release memory related to UI elements, such as bitmap caches.
            }
            if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
                // Release memory related to background processing, such as by
                // closing a database connection.
            }
        }
    }
}

ProfilingManager

Presentada en Android 15 (nivel de API 35), la ProfilingManager API permite que las aplicaciones capturen instantáneas definidas de forma programática (como perfiles de montón, registros del sistema y volcados de montón de Java) directamente en el tiempo de ejecución.

Los desarrolladores pueden activar capturas de forma manual en escenas específicas o registrar activadores automáticos, como TRIGGER_TYPE_ANOMALY, para activar automáticamente una captura cuando el proceso del juego infringe los umbrales del limitador de memoria. Sin embargo, los desarrolladores de juegos deben tener en cuenta las limitaciones críticas en los motores de juegos modernos:

Nota: Los motores de juegos modernos (como Unity o Unreal) administran el rendimiento de la ejecución mediante la asignación previa de grandes bloques de memoria virtual del kernel con mmap con la marca MAP_ANONYMOUS. Luego, los motores usan subasignadores personalizados (por ejemplo, el administrador de memoria nativa de Unity o los BinnedAllocators de Unreal) para subdividir y asignar bloques de memoria de forma interna.

ApplicationExitInfo

Si tu juego finaliza en segundo plano o se detiene porque infringió los límites de memoria de procesos individuales, los mecanismos estándar de volcado de fallas de Java o nativos (como Firebase Crashlytics) no registran el evento. Para consultar y registrar estas finalizaciones de forma programática, los desarrolladores deben aprovechar la ApplicationExitInfo API al inicio del juego.

  • Implementación: Al inicio, llama a ActivityManager.getHistoricalProcessExitReasons() para recuperar los motivos de salida de las sesiones recientes.
  • Motivos clave de salida de memoria:
    • REASON_LOW_MEMORY: Indica que el proceso finalizó debido al LMK del sistema. Esta finalización se produce cuando la presión de la memoria en todo el dispositivo es alta y el SO debe recuperar la RAM. Este motivo de salida indica que el espacio en segundo plano de tu juego es demasiado grande para coexistir con otras aplicaciones.
    • REASON_MEMORY_LIMITER (Android 17 [nivel de API 37] y versiones posteriores): Indica que el proceso se detuvo específicamente porque superó su límite de memoria de cgroup (RssAnon + VmSwap) asignado por el limitador de memoria de la plataforma. Esta finalización puede ocurrir incluso si queda suficiente memoria física en el dispositivo, lo que significa una infracción directa de los límites de procesos individuales.

Usa las herramientas disponibles

Usa las siguientes herramientas de la plataforma durante el desarrollo y el control de calidad para medir con precisión el uso de memoria de tu juego.

meminfo

Esta herramienta recopila estadísticas de memoria a fin de mostrar qué cantidad de memoria PSS se asignó y las categorías para las que se usó.

Imprime las meminfo estadísticas de una de las siguientes maneras:

  • Usa el comando adb shell dumpsys meminfo package-name.
  • Usa la MemoryInfo llamada de la API de Android Debug.

La estadística PrivateDirty muestra la cantidad de memoria RAM dentro del proceso que no se puede paginar en el disco y que no se comparte con ningún otro proceso. La mayor parte de esta cantidad estará disponible para el sistema cuando se detenga ese proceso.

Puntos de seguimiento de memoria

Los puntos de seguimiento de memoria hacen un seguimiento de la cantidad de memoria RSS que usa tu juego. El cálculo del uso de memoria RSS es mucho más rápido que el del uso de PSS. Debido a que es más rápido calcularlo, RSS muestra un nivel de detalle mayor de los cambios en el tamaño de la memoria a fin de obtener mediciones más precisas del uso máximo de memoria. Por lo tanto, resulta más fácil advertir los máximos que podrían hacer que el juego se quede sin memoria.

Perfetto

Perfetto es un paquete de herramientas para recopilar información de rendimiento y memoria en un dispositivo y mostrarla en una IU basada en la Web. Admite registros arbitrariamente largos, por lo que puedes ver cómo el RSS cambia con el tiempo. También puedes emitir búsquedas de SQL sobre los datos que produce para su procesamiento sin conexión. Habilita los registros largos desde la app de Registro del sistema. Asegúrate de que la memory:Memory categoría esté habilitada para el registro. Para la instrumentación de memoria personalizada en el desarrollo y las pruebas, también puedes usar la API de heapprofd (beta).

Inspecciona RssAnon e intercambia en Perfetto

Para verificar el impacto del intercambio de memoria anónima y zRAM de tu juego, carga el archivo de registro en la IU basada en la Web en ui.perfetto.dev y sigue estas técnicas de análisis, diseñadas para estudios de caso de memoria detallados (consulta Estudios de caso de análisis de memoria de Perfetto para obtener más detalles):

1. Visualización de contadores de memoria en el cronograma

  • Ubica tu proceso: En la lista de navegación, busca el nombre del paquete o del proceso de tu juego.
  • Expande el grupo de seguimiento: Haz clic en la fila del proceso para expandir sus seguimientos de subprocesos y ubica el subgrupo llamado Memoria.
  • Analiza los seguimientos:
    • mem.rss.anon (RSS anónimo): Este gráfico de líneas muestra la RAM física en tiempo real ocupada por los grupos de memoria no administrada de tu juego. Supervisa este cronograma durante las cargas de escenas, las ventanas emergentes de la IU o las transiciones de jugabilidad para verificar si hay picos de asignación altos.
    • mem.swap (intercambio comprimido o VmSwap): Este gráfico traza el tamaño precomprimido de los bloques de memoria que se mueven a zRAM. La alta actividad de intercambio que coincide con la jugabilidad indica que tu juego se ejecuta en un dispositivo con memoria restringida y que el sistema comprime de forma activa los recursos en segundo plano.

2. Ejecución de consultas de SQL (procesador de registros) Para realizar un análisis detallado sin conexión, puedes ejecutar consultas de SQL directamente dentro de la consola de la IU de Perfetto o usar la biblioteca de Python del procesador de registros independiente para calcular los picos estadísticos.

  • Busca la asignación máxima de RSS anónimo:

    SELECT
      max(value) / 1024 / 1024 AS max_rss_anon_mb
    FROM counter
    JOIN counter_track ON counter.track_id = counter_track.id
    WHERE counter_track.name = 'mem.rss.anon'
      AND counter_track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      );
    
  • Correlaciona RssAnon y VmSwap en cualquier marca de tiempo:

    SELECT
      ts,
      track.name AS metric_type,
      value / 1024 / 1024 AS size_mb
    FROM counter
    JOIN counter_track track ON counter.track_id = track.id
    WHERE (track.name = 'mem.rss.anon' OR track.name = 'mem.swap')
      AND track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      )
    ORDER BY ts ASC;
    

Para obtener más detalles sobre la inspección de archivos de registro con Android Studio, consulta Inspecciona los registros del sistema: Memoria de proceso (RSS). Para obtener detalles sobre la creación de perfiles de memoria, consulta Registra asignaciones nativas.

heapprofd

heapprofd es una herramienta de seguimiento de memoria que forma parte de Perfetto. Esta herramienta puede ayudarte a encontrar fugas de memoria, ya que muestra dónde se asignó la memoria por medio de malloc. heapprofd puede iniciarse con una secuencia de comandos de Python y, dado que la herramienta tiene una sobrecarga baja, no afecta el rendimiento como lo hacen otras herramientas como Malloc Debug.

bugreport

bugreport es una herramienta de registro cuyo fin es descubrir si tu juego falló o no por quedarse sin memoria. El resultado de la herramienta es mucho más detallado que si usaras logcat. Resulta útil para la depuración de memoria, ya que muestra si el juego falló porque se quedó sin memoria insuficiente o si el LMK lo detuvo.

Para obtener más información, consulta Cómo capturar y leer informes de errores.

Herramientas del motor de juego

Si bien los registros a nivel de la plataforma y la telemetría del sistema son fundamentales para realizar un seguimiento de los umbrales y el cumplimiento del SO, las herramientas específicas del motor de juego te ayudan a atribuir las asignaciones directamente a los objetos del juego, los comportamientos de la secuencia de comandos y las jerarquías de escenas activas.

Unity

En un entorno de Unity Engine, puedes estimar con precisión el espacio en memoria de Android Anonymous RSS + Swap en el tiempo de ejecución con alta confiabilidad (por lo general, muestra una variación de menos del 10% en comparación con los valores verdaderos a nivel del SO) con las herramientas y clases de generación de perfiles nativas de Unity.

Para obtener un instructivo completo paso a paso, incluidas las reglas de configuración y las secuencias de comandos de tiempo de ejecución, consulta Cómo verificar la memoria con las herramientas de Unity.

  • API de Unity Profiler: Puedes aproximar de forma programática el espacio en memoria no administrado de tu juego en el tiempo de ejecución consultando las métricas principales del motor:
    • Usa la clase Profiler: Realiza un seguimiento de las asignaciones totales de memoria sumando los valores de Profiler.GetTotalReservedMemoryLong() y Profiler.GetMonoHeapSizeLong().
    • Usa la clase ProfilerRecorder: Supervisa las categorías de memoria de forma dinámica. Para establecer una aproximación de línea de base confiable, recupera la memoria reservada total (en compilaciones de lanzamiento) o resta la memoria reservada de Gfx (en compilaciones de desarrollo) para quitar los componentes de memoria de gráficos respaldados por archivos.
  • Generador de perfiles de memoria de Unity: Para identificar y depurar fugas de memoria sin conexión, captura una instantánea de memoria y examina el gráfico de memoria residente en el dispositivo que se encuentra en la sección Toda la memoria. Para calcular el espacio aproximado, suma los totales de las siguientes categorías: Sin seguimiento, Android Runtime, Native y Managed.
    • Limitación de zRAM: En condiciones de memoria ajustada, el kernel de Android puede comprimir las páginas de memoria inactivas en el espacio de intercambio (zRAM). Debido a que el Generador de perfiles de memoria de Unity no puede detectar parámetros de intercambio a nivel del SO, es posible que veas pequeñas discrepancias en el espacio durante las escenas de memoria pesada. Haz una referencia cruzada de tus estimaciones con Perfetto para confirmar los valores exactos.