Una vez que comprendas la administración de memoria de Android y hayas configurado las herramientas para medir el uso de memoria de tu juego, el siguiente paso es reducirlo y optimizarlo de forma activa. Respetar los límites estrictos de Android ayuda a evitar que el sistema cierre tu juego, detiene los tiempos de inicio prolongados y garantiza que el juego se ejecute correctamente en todos los dispositivos.
En esta guía, se proporcionan técnicas prácticas para reducir el espacio en memoria de tu juego, enfocándose específicamente en la optimización a nivel de los recursos, las configuraciones específicas del motor y las prácticas recomendadas de administración de la memoria.
Reduce la memoria en Unity
Debido al diseño arquitectónico de Unity, una vez que se expanden los asignadores de bloques nativos y el montón administrado, el motor tiende a conservar esas páginas de memoria para su reutilización en lugar de devolverlas de inmediato al sistema operativo (SO), incluso después de que se liberan los recursos. En particular, el espacio de direcciones virtuales (memoria reservada) permanece reservado durante la vida útil del proceso, y la memoria física (RSS) no se recupera de inmediato hasta que se producen varios ciclos de recolección de elementos no utilizados (GC) y de recorte. En consecuencia, un pico de memoria temporal puede hacer que la memoria residente permanezca inflada durante un tiempo prolongado, incluso después de que disminuya el uso real. Este comportamiento aumenta el riesgo de fallas por falta de memoria (OOM) en dispositivos de gama baja y degrada la estabilidad general del tiempo de ejecución.
Por lo tanto, la optimización de la memoria de Unity debe abordarse a través de tres pilares principales adaptados a estos comportamientos del motor:
- Controla el uso de memoria activa para evitar picos de memoria en primer lugar.
- Administra los formatos de textura y las variantes de sombreador para garantizar que no se creen instancias de recursos nativos y recursos innecesarios.
- Refactoriza las estructuras de código del tiempo de ejecución para eliminar las asignaciones innecesarias en el montón administrado y minimizar la frecuencia de GC y la expansión del montón.
Para obtener más información, consulta Optimización de la memoria de Unity.
Reduce la memoria en Unreal Engine
En Unreal Engine, las canalizaciones de renderización de alta fidelidad y los gráficos complejos de dependencia de objetos pueden aumentar significativamente la presión de la memoria RSS anónima y la memoria respaldada por archivos. En particular, depender de referencias físicas y jerarquías de herencia de Blueprint profundas obliga a cargar en la memoria los recursos conectados no utilizados. Además, las permutaciones excesivas de sombreadores, los grupos de transmisión de texturas no optimizados y las tablas de reubicación ELF sin comprimir contribuyen a generar huellas de memoria de referencia altas, lo que aumenta el riesgo de finalizaciones de Low Memory Killer (LMK).
Por lo tanto, la optimización de la memoria de Unreal Engine debe abordarse a través de tres pilares principales adaptados a estos comportamientos del motor:
- Desvincula los datos y la lógica, y reemplaza las referencias físicas o fuertes por referencias débiles o flexibles.
- Quita las funciones de iluminación para dispositivos móviles y las opciones de permutación no utilizadas para minimizar los objetos de estado de canalización (PSO) y los objetivos de renderización redundantes, mientras aplicas la compresión ASTC y personalizas los grupos de transmisión de texturas con perfiles de dispositivos.
- Habilita la compresión de la tabla de reubicación RELR y APS para reducir el tamaño binario de ELF y la huella de memoria física del tiempo de ejecución.
Para obtener más información, consulta Optimización de la memoria de Unreal.
Optimización de varios procesos
El uso de memoria de los procesos almacenados en caché se excluye del cálculo de los límites de memoria porque no tiene impacto en las apps activas. Ejecutar el servicio en un proceso aislado independiente ayuda al proceso principal a realizar la transición a un estado almacenado en caché lo más rápido posible, lo que mejora el estado de tu juego.
Para obtener más información, consulta Cómo hacer un seguimiento del estado y la memoria del proceso, Cómo aislar un proceso de servicio con Unity y Cómo aislar un proceso de servicio con Unreal.
Reduce el uso de memoria en los servicios percibidos por el usuario
Es posible que tu juego deba ejecutar lógica en un servicio percibido por el usuario para casos de uso como completar una descarga grande o para sistemas de chat de voz en segundo plano. Estas estrategias pueden ayudarte a administrar y reducir el uso de memoria durante estos casos.
Estrategias para descargas grandes
Estas estrategias se pueden aplicar a descargas grandes que deseas continuar incluso después de que el usuario minimice tu juego.
1. Aísla el proceso de descarga
Qué: Asegúrate de que el SO pueda recuperar de inmediato la memoria que no usa tu app realizando la descarga en un proceso independiente, ya que es posible que el asignador de memoria conserve las páginas del grupo de memoria y mantenga la memoria RSS anónima artificialmente alta incluso después de liberar los arrays. Cuando finalizas o sales del servicio y el proceso de forma explícita, la memoria se devuelve al grupo del SO y el proceso principal no se verá afectado.
En Unity: Transfiere la descarga a un Android nativo
Servicedeclarado con un proceso comoandroid:process=":downloader"en un manifiesto personalizado y actívalo con la JNIAndroidJavaClassde Unity. Asegúrate de finalizar el proceso cuando se complete la descarga. Consulta Ejecuta un servicio perceptible en un proceso independiente con Unity para obtener instrucciones más detalladas.En Unreal: Declara un Android personalizado
Servicecon un proceso comoandroid:process=":downloader"con Unreal Plugin Language y actívalo con la JNI de C++. Asegúrate de finalizar el proceso cuando se complete la descarga. Consulta Ejecuta un servicio perceptible en un proceso independiente con Unreal para obtener instrucciones más detalladas.Para Android nativo: Declara un
Serviceen elAndroidManifestcon un proceso comoandroid:process=":downloader". Ejecuta la descarga en este proceso aislado y llama aProcess.killProcess(Process.myPid())cuando se complete la descarga.
Cómo ayuda: Reduce la duración durante la que se mantiene la memoria y permite que la descarga continúe mientras se libera la memoria que usa el proceso principal más grande.
2. Transmite descargas directamente al disco
Qué: Transmite datos directamente desde el socket de red al disco usando un búfer reutilizable pequeño de tamaño fijo en lugar de acumular respuestas de red en un array grande antes de escribirlo.
En Unity: Evita usar
DownloadHandlerBufferpara paquetes de recursos o archivos grandes, ya que asigna un búfer de memoria nativo equivalente al tamaño del archivo (memoria RSS anónima). En su lugar, usaDownloadHandlerFilepara transmitir bytes de forma nativa al disco en un subproceso en segundo plano.En Unreal: Canaliza los fragmentos de datos entrantes directamente a un
FArchive(archivo respaldado por archivos que usa el Administrador de archivos de Unreal) conSetResponseBodyReceiveStream()en lugar de agregar cargas útiles de unIHttpRequesta unTArray<uint8>.Para Android nativo: Canaliza el
InputStreama unFileOutputStreamcon un búfer agrupado en lugar de llamar a.readBytes()o.string()en una respuesta HTTP.
Cómo ayuda: Reduce el uso máximo de memoria.
3. Transmite la descompresión de archivos descargados
Qué: Si la descarga está comprimida, incluye tu flujo de entrada de red en un
descompresor de transmisión como ZipInputStream en lugar de descargar el
archivo, cargarlo en la RAM y, luego, extraerlo.
Cómo ayuda: Reduce el uso máximo de memoria.
4. Delega al SO
Qué: Para evitar administrar la memoria en segundo plano por completo, delega el trabajo a las APIs nativas de Android.
WorkManageres el wrapper moderno y recomendado alrededor deJobSchedulera nivel del SO. En Android 14 y versiones posteriores,WorkManagercontrola automáticamente las descargas activadas por el usuario como una tarea de transferencia de datos iniciada por el usuario (UIDT). Esto se ejecuta dentro del proceso de tu app, por lo que debes transmitir directamente al disco para minimizar el uso de memoria. UIDT protege tu app de las fallas por falta de memoria, ya que permite que el SO pause y reanude la descarga correctamente si los recursos del sistema se vuelven limitados.DownloadManagerse ejecuta en un proceso del sistema independiente y no atribuye el uso de memoria para la descarga a tu app. Tu app recibirá una notificación de transmisión cuando el archivo se descargue y esté listo.
Cómo ayuda: WorkManager ayuda a lidiar con situaciones de poca memoria, y DownloadManager reduce el uso de memoria de tu app.
Estrategias para servicios auxiliares
Estas estrategias se pueden aplicar a los servicios auxiliares que ejecuta un juego en paralelo con el proceso principal del juego, como el chat de voz en segundo plano.
1. Aísla el proceso
Qué: Desvincula la función, por ejemplo, tu solución de chat de voz, del motor de juego principal. Por ejemplo, puedes ejecutar la captura del micrófono y
la transmisión de red dentro de un servicio en primer plano de Android asignado a un proceso independiente (declarado en el manifiesto, como con android:process=":voice").
Cómo ayuda: Cuando se minimiza la app, el proceso principal del juego pesado puede pasar a un estado almacenado en caché de menor prioridad mientras que el servicio auxiliar más liviano continúa en el estado de servicio percibido por el usuario.
2. Recorta la memoria no utilizada en el proceso
Qué: Si el servicio auxiliar está demasiado integrado en el motor del juego para separarlo, intenta reducir la mayor cantidad de peso del proceso tan pronto como el juego se ejecute en segundo plano o se minimice. Considera vaciar las cachés de texturas, descargar escenas no esenciales, reducir las tasas de renderización y de ticks del motor a 0 y llamar explícitamente a la recolección de elementos no utilizados.
En Unity: Realiza este recorte cuando se active
OnApplicationPause().Resources.UnloadUnusedAssets()puede ser útil.En Unreal: Vincula la lógica de recorte al
ApplicationWillEnterBackgroundDelegatedelegado.Para Android nativo: Realiza el recorte en
onPause()oonStop()según corresponda. Es posible que el SO intente llamar a tu implementación deonTrimMemory()antes de recurrir a la finalización de procesos con un uso de memoria alto.
Cómo ayuda: Reduce el uso de memoria que no es necesario cuando el juego no está en primer plano.