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.
Información sobre 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 administración de la memoria. Para obtener información detallada sobre cómo se procesa y supervisa este parámetro de telemetría en la práctica, 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 que ocupa un proceso que se mantiene en la RAM física del dispositivo. El RSS se divide en memoria respaldada por archivos y memoria anónima. La métrica de memoria de Android se enfoca estrictamente en el RSS anónimo:
- Qué incluye: Páginas de memoria asignadas directamente por el proceso del 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 motores de C++ personalizados o bloques de memoria solicitados con malloc nativo o nuevos 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 grupos de memoria nativa masivos para controlar la física, la renderización y la lógica. Debido a que estos grupos no están respaldados por archivos, viven por completo en el RSS anónimo y forman la mayor parte de la huella física de tu juego.
2. Intercambio sin comprimir (VmSwap)
Android no admite un espacio de intercambio tradicional basado en discos 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 la 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 hace 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 la zRAM, esta sigue contando para el espacio en memoria total del juego.
3. Estados de los procesos
El uso de memoria se desglosa por estados de proceso en Android vitals. En el caso de los desarrolladores de juegos, los SDK o juegos de terceros también pueden activar servicios percibidos por el usuario o servicios en segundo plano de forma inesperada.
- Qué incluye: Servicios en primer plano, perceptibles, en segundo plano y almacenados en caché.
- Por qué es importante: Los diferentes estados de los procesos tienen distintos impactos en la administración de la memoria del SO Android. Es posible que no sepas que tu juego se está ejecutando con un estado de proceso sensible si alguno de los SDK 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 memoria y capture diagnósticos detallados de la memoria durante el tiempo de ejecución.
Cómo responder a eventos de recorte de memoria
El sistema usa onTrimMemory para notificar a tu app sobre los eventos del ciclo de vida que presentan una buena oportunidad para que tu app reduzca voluntariamente su uso de memoria y evite que el optimizador de poca memoria (LMK) la cierre para liberar memoria que puedan usar otras apps.
Si el sistema finaliza tu app en segundo plano, el usuario experimentará un inicio en frío lento cuando se reanude. Reducir el uso de memoria en segundo plano ayuda a evitar estas finalizaciones en segundo plano.
Cuando respondas a eventos de recorte, libera las asignaciones de memoria grandes y reconstruibles que no se necesiten de inmediato:
Ejemplo: Recorta o borra mapas de bits almacenados en caché (decodificados desde el 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 {
@Override
public void onTrimMemory(int 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
La API de ProfilingManager, que se introdujo en Android 15 (nivel de API 35), permite que las aplicaciones capturen instantáneas definidas de manera programática (como perfiles de montón, seguimientos del sistema y volcados de montón de Java) directamente durante 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 incumple los umbrales del limitador de memoria. Sin embargo, los desarrolladores de juegos deben tener en cuenta las limitaciones fundamentales de los motores de juego modernos:
Nota: Los motores de juegos modernos (como Unity o Unreal) administran el rendimiento de la ejecución asignando previamente bloques de memoria virtual masivos desde el kernel con mmap y la marca MAP_ANONYMOUS. Luego, los motores usan subasignadores personalizados (por ejemplo, el administrador de la memoria nativo de Unity o los BinnedAllocators de Unreal) para subdividir y asignar bloques de memoria de forma interna.
ApplicationExitInfo
Si tu juego se cierra en segundo plano o se finaliza porque infringió los límites de memoria de procesos individuales, los mecanismos estándar de volcado de falla por error en código nativo o de Java (como Firebase Crashlytics) no registran el evento. Para consultar y registrar estas finalizaciones de manera programática, los desarrolladores deben aprovechar la API de ApplicationExitInfo cuando se inicia el juego.
- Implementación: Al inicio, llama a
ActivityManager.getHistoricalProcessExitReasons()para recuperar los motivos de salida de las sesiones recientes. - Motivos clave de salida de la memoria:
REASON_LOW_MEMORY: Indica que el Low Memory Killer (LMK) del sistema finalizó el proceso. Esta finalización se produce cuando la presión de la memoria en todo el dispositivo es alta y el SO debe reclamar 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 QPR2, nivel de API 37.2/Build.VERSION_CODES_FULL.CINNAMON_BUN_2y versiones posteriores): Indica que el proceso se finalizó específicamente porque superó el 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 mucha memoria física en el dispositivo. En las compilaciones anteriores de Android 17,getReason()devuelveREASON_OTHERcon"MemoryLimiter:AnonSwap"engetDescription().
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 para mostrar cuánta memoria de PSS se asignó y las categorías para las que se usó.
Imprime las estadísticas de meminfo de una de las siguientes maneras:
- Usa el comando
adb shell dumpsys meminfo package-name: - Usa la llamada
MemoryInfode la API de Android Debug.
La estadística PrivateDirty muestra la cantidad de 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 este importe queda disponible para el sistema cuando se finaliza ese proceso.
Puntos de seguimiento de memoria
Los puntos de seguimiento de memoria registran la cantidad de memoria RSS que usa tu juego. Calcular el uso de memoria RSS es mucho más rápido que calcular el uso de PSS. Debido a que es más rápido de calcular, el RSS muestra un nivel de detalle más preciso sobre los cambios en el tamaño de la memoria para obtener mediciones más exactas del uso de memoria máximo. Por lo tanto, es más fácil notar los picos que podrían hacer que el juego se quede sin memoria.
Perfetto
Perfetto es un paquete de herramientas que permite recopilar información de rendimiento y memoria en un dispositivo, y mostrarla en una IU basada en la Web. Admite seguimientos de cualquier duración para que puedas ver cómo cambia el RSS con el tiempo. También puedes
emitir consultas en SQL sobre los datos que produce para el procesamiento sin conexión. Habilita los registros largos desde la app de Registro del sistema. Asegúrate de que la categoría memory:Memory 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 y el intercambio en Perfetto
Para verificar el impacto de la memoria anónima y el intercambio de 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 analíticas, diseñadas para estudios de casos de memoria profunda (consulta los estudios de casos de análisis de memoria de Perfetto para obtener más detalles):
1. Visualiza los 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.
- Expandir el grupo de pistas: Haz clic en la fila del proceso para expandir sus pistas de subprocesos y busca el subgrupo llamado Memory.
- Analiza las pistas:
- mem.rss.anon (RSS anónimo): Este gráfico de líneas muestra la RAM física en tiempo real que ocupan los grupos de memoria no administrados de tu juego. Supervisa esta línea de tiempo durante las cargas de escenas, las ventanas emergentes de la IU o las transiciones de juego para verificar si hay picos de asignación altos.
- mem.swap (intercambio comprimido o VmSwap): En este gráfico, se muestra el tamaño previo a la compresión de los bloques de memoria que se movieron a zRAM. La alta actividad de intercambio que coincide con el juego indica que este se ejecuta en un dispositivo con memoria limitada y que el sistema comprime de forma activa los recursos en segundo plano.
2. Ejecución de consultas en SQL (Trace Processor): Para realizar un análisis detallado sin conexión, puedes ejecutar consultas en SQL directamente en la consola de la IU de Perfetto o usar la biblioteca independiente de Trace Processor para Python para calcular los picos estadísticos.
Encuentra 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 determinada:
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 cómo inspeccionar archivos de registro con Android Studio, consulta Cómo inspeccionar registros del sistema: memoria de proceso (RSS). Para obtener detalles sobre cómo crear secuencias de comandos de perfiles de memoria, consulta Cómo registrar 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ó memoria con malloc. heapprofd se puede iniciar con una secuencia de comandos de Python y, como la herramienta
tiene una sobrecarga baja, no afecta el rendimiento como 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 el uso de logcat. Es útil para la depuración de memoria, ya que muestra si el juego falló porque se quedó sin memoria o si lo finalizó el LMK.
Si deseas obtener más información, consulta Cómo capturar y leer informes de errores.
Herramientas de motores de juego
Si bien los registros a nivel de la plataforma y la telemetría del sistema son fundamentales para hacer 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 las secuencias 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 RSS anónimo + espacio de intercambio de Android en el entorno de ejecución con una alta confiabilidad (por lo general, se muestra una variación de menos del 10% en comparación con los valores reales a nivel del SO) con las herramientas y clases de generación de perfiles nativas de Unity.
Para obtener un instructivo paso a paso completo, incluidas las reglas de configuración y las secuencias de comandos del entorno de ejecución, consulta Cómo verificar la memoria con las herramientas de Unity.
- API de Unity Profiler: Puedes aproximar de manera programática el espacio en memoria no administrada de tu juego durante el tiempo de ejecución consultando las métricas principales del motor:
- Usa la clase Profiler: Realiza un seguimiento de las asignaciones de memoria totales sumando los valores de
Profiler.GetTotalReservedMemoryLong()yProfiler.GetMonoHeapSizeLong(). - Usa la clase
ProfilerRecorder: Supervisa las categorías de memoria de forma dinámica. Para establecer una aproximación de referencia confiable, recupera la memoria reservada total (en las compilaciones de lanzamiento) o réstale la memoria reservada de Gfx (en las compilaciones de desarrollo) para quitar los componentes de memoria de gráficos respaldados por archivos.
- Usa la clase Profiler: Realiza un seguimiento de las asignaciones de memoria totales sumando los valores de
- Generador de perfiles de memoria de Unity: Para identificar y depurar fugas de memoria sin conexión,
captura una instantánea de memoria y, luego, inspecciona el gráfico Resident Memory on Device
que se encuentra en la sección All of Memory. Para calcular el espacio aproximado,
suma los totales de las siguientes categorías: Sin seguimiento, Android
Runtime, Nativo y Administrado.
- Limitación de zRAM: En condiciones de memoria ajustada, el kernel de Android puede comprimir 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 los parámetros de intercambio a nivel del SO, es posible que veas discrepancias menores en el consumo de memoria durante las escenas con un uso intensivo de memoria. Compara tus estimaciones con Perfetto para confirmar los valores exactos.
Unreal
En un entorno de Unreal Engine, puedes evaluar el espacio en memoria de tu juego combinando los diagnósticos del motor con la telemetría de la plataforma. Para obtener instrucciones paso a paso y flujos de trabajo de generación de perfiles, consulta Cómo verificar el uso de memoria con Unreal Engine.
Entre las herramientas y las interfaces de diagnóstico clave, se incluyen las siguientes:
- API de diagnóstico de C++: Usa
GetMemoryUsedFastpara realizar consultas de memoria ligeras y la interfazGetStatspara obtener estadísticas de memoria a nivel del hardware. - Comandos de la consola: Supervisa las tendencias de asignación de memoria en tiempo real en el hardware del dispositivo con los comandos del motor
stat unitystat unitmax. - Unreal Insights: Inspecciona capturas de cronogramas con precisión de fotogramas para analizar métricas a nivel de la plataforma y contadores de memoria personalizados.