En esta página, se explica cómo puedes reducir de manera proactiva el uso de memoria dentro de tu app. Si quieres obtener información sobre cómo administra la memoria el sistema operativo Android, consulta la Descripción general de la administración de la memoria.
La memoria de acceso aleatorio (RAM) es un recurso valioso en cualquier entorno de desarrollo de software, y es aún más valiosa en un sistema operativo móvil donde la memoria física suele tener restricciones. Aunque tanto Android Runtime (ART) como la máquina virtual Dalvik realizan de manera rutinaria la recolección de elementos no utilizados, esto no significa que puedas ignorar en qué momento y lugar tu app asigna y libera memoria. Debes evitar las fugas de memoria, que en general son causadas por la retención de referencias de objetos en variables de miembros estáticas, y debes liberar cualquier objeto Reference en el momento apropiado, según lo definido por las devoluciones de llamada de ciclo de vida.
Cómo reducir la huella de recursos y código de tu app
Dentro del código, algunos recursos y bibliotecas pueden consumir memoria sin que te des cuenta. El tamaño general de tu app, incluidos los recursos integrados o las bibliotecas de terceros, puede afectar la cantidad de memoria que consume la app. Puedes mejorar el consumo de memoria de tu app quitando de tu código cualquier componente, recurso o biblioteca redundante, innecesario o ampliado.
Reduce el tamaño general de la app habilitando R8
El código de la aplicación compilada es una parte activa de la huella de memoria del tiempo de ejecución. Cada clase, método, dependencia de biblioteca y constante de cadena debe cargarse en la RAM cuando se ejecuta. Cuanto más grande sea tu base de código compilada, más RAM física requerirá tu app.
Puedes usar R8 para reducir el espacio en memoria de tu app. Si bien R8 se conoce tradicionalmente por reducir el tamaño del APK, tiene un impacto positivo y directo en la memoria de tiempo de ejecución (RAM). R8 analiza el código de bytes de tu app para quitar el código no utilizado, combinar clases redundantes, insertar métodos y minimizar identificadores. Al cargar menos código de bytes compilado del APK en la RAM, se reduce el espacio en memoria general de referencia de la app. Además, la minificación de los nombres de clases, métodos y campos en identificadores más cortos reduce directamente la sobrecarga de RAM. Las optimizaciones, como la combinación de clases y la intercalación de métodos extensos, también reemplazan las búsquedas y los patrones de asignación costosos en el tiempo de ejecución, lo que genera una optimización de la memoria de pila y del montón.
Información sobre las reglas de conservación
Las reglas de conservación son instrucciones de configuración que le indican a R8 qué partes del código debe conservar durante la optimización, lo que evita que quite o reduzca el código del que depende tu app. Para obtener más información, consulta la Descripción general de las reglas de conservación.
Las reglas de conservación mal escritas impiden que R8 optimice grandes porciones de tu base de código. Evita las reglas de conservación demasiado amplias y sigue estas prácticas recomendadas:
- Reglas globales que se deben evitar:
-dontoptimize: Inhabilita por completo la optimización para toda la app, lo que genera ejecutables más grandes y lentos.-dontshrink: Evita la eliminación del código y los recursos no utilizados.-dontobfuscate: Evita la minificación de nombres, lo que impide ahorrar memoria valiosa (especialmente en apps grandes).
Evita los comodines en todo el paquete: Las reglas amplias, como
-keep class com.example.package.** { *; }, obligan a R8 a conservar cada clase, campo y método de ese paquete. Esto detiene por completo la capacidad de R8 para quitar, optimizar o minimizar el código en ese paquete.Usa el archivo de configuración predeterminado de R8: Siempre usa
proguard-android-optimize.txt.
Para obtener más información sobre cómo escribir reglas de conservación, consulta la Descripción general de las reglas de conservación. Para conocer los patrones específicos que debes usar y evitar, consulta las prácticas recomendadas para las reglas de conservación.
El Analizador de configuración de R8 proporciona estadísticas sobre tu configuración de R8 y cómo cada regla de conservación afecta tu app. Para obtener más información sobre cómo identificar las reglas que bloquean la optimización, consulta Analizador de configuración de R8.
Ten cuidado cuando uses bibliotecas externas
Por lo general, el código de la biblioteca externa no está escrito para entornos móviles y puede ser ineficiente para trabajar en un cliente móvil. Cuando utilizas una biblioteca externa, es probable que debas optimizarla para dispositivos móviles. Planifica ese trabajo por adelantado y analiza la biblioteca en términos de tamaño de código y uso de RAM antes de usarla.
Incluso algunas bibliotecas optimizadas para dispositivos móviles pueden causar problemas debido a las diferentes implementaciones. Por ejemplo, una biblioteca podría usar protobufs lite, mientras que otra usa microprotobufs, por lo que se obtienen dos implementaciones de protobuf diferentes en tu app. Esto puede ocurrir con diferentes implementaciones de registro, análisis, frameworks de carga de imágenes, almacenamiento en caché y muchas otras cosas que no esperas.
Si bien optimizar tu app con R8 puede quitar el código no utilizado de las dependencias, su eficacia suele estar limitada por la configuración interna de la biblioteca. Por ejemplo, las reglas de conservación amplias o el uso de la reflexión dentro de una biblioteca pueden impedir que R8 reduzca su código, lo que genera una mayor huella de memoria. Para obtener estrategias sobre cómo seleccionar bibliotecas eficientes, consulta Cómo elegir bibliotecas de forma inteligente.
Evita usar una biblioteca compartida para una o dos funciones de las docenas de funciones disponibles. No extraigas una gran cantidad de código y sobrecarga que no usarás. Cuando consideres usar una biblioteca, busca una implementación que se ajuste a lo que necesites. Como alternativa, puedes optar por crear tu propia implementación.
Usa Hilt o Dagger 2 para la inserción de dependencias
Los frameworks de inyección de dependencias pueden simplificar el código que escribes y proporcionar un entorno adaptativo que sea útil para las pruebas y otros cambios de configuración.
Si piensas usar un framework de inyección de dependencias en tu app, considera usar Hilt o Dagger. Hilt es una biblioteca de inserción de dependencias para Android que se ejecuta en Dagger. Dagger no usa reflexión para escanear el código de tu app. Puedes usar la implementación estática y en tiempo de compilación de Dagger en apps para Android sin costos de tiempo de ejecución ni uso de memoria innecesarios.
Otros frameworks de inyección de dependencias que utilizan reflexión inicializan procesos con la búsqueda de anotaciones en tu código. Este proceso puede requerir muchos más ciclos de CPU y RAM, y puede causar un retraso notable cuando se inicia la app.
Cuando uses la inyección de dependencias, ten cuidado de evitar las fugas de memoria. Para ello, asegúrate de que los objetos tengan el alcance adecuado. Retener objetos más tiempo del necesario vinculándolos al ciclo de vida incorrecto puede provocar fugas de memoria. Para obtener más información, consulta la guía sobre cómo evitar pérdidas de memoria con objetos de alcance.
Carga imágenes de forma intencional
Por lo general, los mapas de bits gráficos son los objetos comunes más grandes que residen en la memoria de tu app. Incluso si trabajas con archivos comprimidos, como JPEG, el archivo debe expandirse en un mapa de bits sin comprimir para mostrarse en la pantalla. Un archivo de imagen comprimido pequeño se puede expandir a un mapa de bits muy grande.
Por ejemplo, la mayoría de los mapas de bits usan la configuración ARGB_8888, lo que significa que cada píxel requiere 4 bytes de memoria: un byte para cada color (rojo, verde y azul) y un byte para el canal alfa (transparencia). Si tienes un archivo JPEG de 100 KB y lo muestras en una vista de 1,000 × 1,000 píxeles, el mapa de bits requerirá 4 bytes para cada uno de esos 1,000,000 de píxeles, lo que suma 4 MB de memoria.
Existen varias acciones que puedes realizar para optimizar el uso de imágenes. Por ejemplo, usar bibliotecas de carga de imágenes puede ayudarte a liberar memoria cuando no se necesita. Para obtener información sobre cómo controlar imágenes de manera eficiente, consulta Cómo optimizar imágenes de mapa de bits.
Cómo supervisar la memoria disponible y el uso de memoria
Para poder solucionar los problemas del uso de la memoria de tu app primero debes encontrarlos. El generador de perfiles de memoria de Android Studio te ayuda a encontrar y diagnosticar problemas de memoria de las siguientes maneras:
- Observa el modo en que tu app asigna memoria en el tiempo. El generador de perfiles de memoria muestra un gráfico en tiempo real de la cantidad de memoria que usa tu app, la cantidad de objetos Java asignados y cuándo se produce la recolección de elementos no utilizados.
- Inicia eventos de recolección de elementos no utilizados y toma una instantánea del montón de Java mientras se ejecuta tu app.
- Registra las asignaciones de memoria de tu app, inspecciona todos los objetos asignados, observa el seguimiento de la pila para cada asignación y salta al código correspondiente en el editor de Android Studio.
El generador de perfiles de memoria también se integra en la biblioteca de detección de fugas LeakCanary. Con LeakCanary, puedes trasladar el análisis de fugas de memoria del dispositivo de prueba a tu máquina de desarrollo, lo que puede acelerar significativamente tu flujo de trabajo. Para obtener más información, consulta las notas de la versión de Android Studio.
Existen otras herramientas que puedes usar para diagnosticar problemas de memoria según los datos de los usuarios que ejecutan tu app de producción:
- Usa Android vitals para hacer un seguimiento de los eventos de cierre por falta de memoria (LMK).
- Usa el Administrador de generación de perfiles para hacer un seguimiento de los errores de memoria insuficiente, así como del comportamiento anómalo de la app que podría deberse a fugas de memoria.
Cómo liberar memoria en respuesta a eventos
Android puede reclamar memoria de tu app o cerrar la app por completo si es necesario para liberar memoria para tareas críticas, como se explica en Descripción general de la administración de memoria. Para ayudar a equilibrar aún más la memoria del sistema y evitar la necesidad de este de detener el proceso de tu app, puedes implementar la interfaz ComponentCallbacks2 en las clases de tu Activity. El método de devolución de llamada onTrimMemory() proporcionado notifica a tu app sobre eventos relacionados con el ciclo de vida o la memoria que representan una buena oportunidad para que tu app reduzca voluntariamente su uso de memoria.
Liberar memoria puede reducir la frecuencia con la que el optimizador de poca memoria detiene tu app.
Tu implementación de onTrimMemory() debe enfocarse exclusivamente en los eventos TRIM_MEMORY_UI_HIDDEN y TRIM_MEMORY_BACKGROUND. (A partir de Android 14, el sistema ya no entrega notificaciones para las otras constantes heredadas). Esas constantes dejaron de estar disponibles formalmente en Android 15.
TRIM_MEMORY_UI_HIDDEN: Este indicador señala que la IU de tu app dejó de estar a la vista del usuario. Esta transición brinda la oportunidad de liberar asignaciones de memoria sustanciales vinculadas estrictamente a la IU, como mapas de bits, búferes de reproducción de video o recursos de animación complejos.TRIM_MEMORY_BACKGROUND: Este indicador señala que tu proceso se encuentra en segundo plano y ahora es candidato para finalizar su ejecución y satisfacer las necesidades globales de memoria del sistema. Para extender la duración en la que tu proceso permanece en el estado almacenado en caché y reducir la cantidad de inicios en frío de la app, debes liberar de forma agresiva todos los recursos que se puedan reconstruir fácilmente una vez que el usuario reanude su sesión.
En esta muestra de código, se muestra cómo implementar la devolución de llamada onTrimMemory() para responder a diferentes eventos relacionados con la memoria:
Kotlin
import android.content.ComponentCallbacks2
// Other import statements.
class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
// Other activity code.
/**
* Release memory when the UI becomes hidden or when system resources become low.
* @param level the memory-related event that is raised.
*/
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
import android.content.ComponentCallbacks2;
// Other import statements.
public class MainActivity extends AppCompatActivity
implements ComponentCallbacks2 {
// Other activity code.
/**
* Release memory when the UI becomes hidden or when system resources become low.
* @param level the memory-related event that is raised.
*/
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.
}
}
}
Comprueba cuánta memoria necesitas
Para permitir la ejecución de varios procesos, Android establece un límite estricto en cuanto al tamaño del montón asignado para cada app. El límite exacto del tamaño del montón varía entre dispositivos según la cantidad de memoria RAM que tenga disponible el dispositivo en general. Si tu app alcanza la capacidad de montón máxima e intenta asignar más memoria, el sistema arroja un OutOfMemoryError.
Para evitar quedarte sin memoria, puedes consultar el sistema para determinar cuánto espacio del montón está disponible en el dispositivo actual. Puedes consultar esta cifra en el sistema llamando a getMemoryInfo(). Se mostrará un objeto ActivityManager.MemoryInfo, que proporciona información sobre el estado actual de la memoria del dispositivo, como su capacidad disponible, su capacidad total y su umbral (el nivel de memoria en el que el sistema comienza a detener procesos). El objeto ActivityManager.MemoryInfo también expone lowMemory, que es un booleano simple que indica si el dispositivo se está quedando sin memoria.
En el siguiente fragmento de código, se muestra un ejemplo de cómo puedes usar el método getMemoryInfo() en tu app.
Kotlin
fun doSomethingMemoryIntensive() {
// Before doing something that requires a lot of memory,
// check whether the device is in a low memory state.
if (!getAvailableMemory().lowMemory) {
// Do memory intensive work.
}
}
// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
return ActivityManager.MemoryInfo().also { memoryInfo ->
activityManager.getMemoryInfo(memoryInfo)
}
}
Java
public void doSomethingMemoryIntensive() {
// Before doing something that requires a lot of memory,
// check whether the device is in a low memory state.
ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();
if (!memoryInfo.lowMemory) {
// Do memory intensive work.
}
}
// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
return memoryInfo;
}
Supervisa los cierres por poca memoria
Los errores de LMK visibles para el usuario ocurren cuando la memoria del sistema se agota de forma crítica. Cuando la memoria es baja, el daemon lmkd (asesino de memoria baja) finaliza los procesos según su oom_adj_score. Las apps que se almacenan en caché o que ejecutan un servicio sin una IU asociada (como un trabajo) tienen las puntuaciones más altas y se cierran primero. Si la memoria sigue siendo críticamente baja, el daemon se ve obligado a reclamar memoria de los procesos con un oom_adj_score de 0.
Como esa puntuación está reservada para las apps visibles, su finalización genera una salida inmediata y no gradual del proceso. Para el usuario final, parece que la app falló, a menudo, sin pasar por los mecanismos estándar de guardado del estado del ciclo de vida, lo que provoca la pérdida del progreso del usuario.
Los cierres de procesos en primer plano son un enfoque principal en Android vitals porque sirven como un proxy de alta fidelidad para la administración incorrecta de la memoria. Si bien cualquier tasa de LMK superior al 1% indica una necesidad crítica de acción inmediata, una tasa baja no es necesariamente un indicador de buen estado. Una tasa baja de LMK percibido por el usuario podría significar que el daemon de LMK cierra procesos con frecuencia mientras se ejecutan en segundo plano, lo que degrada el rendimiento del "inicio semicaliente" y la fluidez de la multitarea. Por lo tanto, te recomendamos que cumplas con las prácticas recomendadas de memoria, independientemente de tu puntuación actual de LMK, para garantizar la estabilidad a largo plazo y el buen estado del dispositivo.
Usa ProfilingManager para hacer un seguimiento de los problemas de memoria
La plataforma de Android proporciona ProfilingManager, una API de observabilidad avanzada que te permite capturar datos del usuario en producción según los activadores que establezcas. Esto puede ayudarte a identificar problemas de memoria difíciles de reproducir.
Dos activadores nuevos introducidos con Android 17 son especialmente útiles para detectar problemas de memoria:
TRIGGER_TYPE_OOMindica que la app lanzó unOutOfMemoryError. Se activa la próxima vez que la app se inicia después de la falla, cuando la app se registra para activar la generación de perfiles.TRIGGER_TYPE_ANOMALYse activa cuando el sistema detecta un comportamiento anómalo de la app. Entre otras cosas, esto se puede activar por un uso excesivo de la memoria. Se activa después de que la app haya mostrado un uso excesivo de memoria, y antes de que el sistema tome medidas para detener el proceso infractor. Por ejemplo, si la app supera los límites de memoria introducidos en Android 17,TRIGGER_TYPE_ANOMALYse activa antes de que el sistema cierre la app.
Para obtener más información sobre el uso de ProfilingManager para registrar y recuperar activadores de forma programática, consulta la documentación sobre el registro basado en activadores.
También puedes usar el registro de perfil basado en la app para definir manualmente los puntos de inicio y finalización del registro. Te recomendamos que lo hagas para capturar manualmente volcados o perfiles de montón en áreas en las que sospeches que podría haber fugas de memoria o un uso excesivo de memoria.
Usa construcciones de código más eficientes en términos de memoria
Algunas funciones de Android, clases de Java y construcciones de código usan más memoria que otras. Puedes minimizar la cantidad de memoria que usa tu app eligiendo alternativas más eficientes en tu código.
Usa los servicios con moderación
Te recomendamos que no dejes los servicios en funcionamiento cuando no sea necesario. Dejar un servicio innecesario en funcionamiento es uno de los peores errores de administración de la memoria que puede cometer una app para Android. Si tu app necesita que un servicio realice el trabajo en segundo plano, no la dejes activa, a menos que deba ejecutar un trabajo. Detén el servicio cuando complete la tarea. De lo contrario, podrías provocar una fuga de memoria.
Cuando inicias un servicio, el sistema prefiere mantener en ejecución el proceso de ese servicio. Este comportamiento hace que los procesos de servicios sean muy costosos, porque la RAM utilizada por un servicio no está disponible para otros procesos. Esto reduce la cantidad de procesos que puede mantener el sistema en la caché LRU, lo que hace que el cambio de apps sea menos eficiente. Incluso puede provocar una hiperpaginación en el sistema cuando la memoria es escasa y este no puede mantener suficientes procesos para alojar a todos los servicios que se ejecutan en ese momento.
En general, evita el uso de servicios persistentes debido a las demandas continuas que realizan a la memoria disponible. En cambio, te recomendamos que utilices una implementación alternativa, como WorkManager.
Si quieres obtener más información para usar WorkManager y programar procesos en segundo plano, consulta Trabajo persistente.
Usa contenedores de datos optimizados
Algunas de las clases que proporciona el lenguaje de programación no están optimizadas para uso en dispositivos móviles. Por ejemplo, la implementación genérica de HashMap puede ser ineficiente en cuanto al uso de memoria porque necesita un objeto de entrada independiente para cada asignación.
El framework de Android incluye varios contenedores de datos optimizados, como SparseArray, SparseBooleanArray y LongSparseArray. Por ejemplo, las clases SparseArray son más eficientes porque evitan la necesidad del sistema de convertir automáticamente la clave y, a veces, el valor, lo que crea otro objeto o dos por entrada.
Si es necesario, siempre puedes usar arrays sin formato para lograr una estructura de datos simple.
Ten cuidado con las abstracciones de código
Los desarrolladores suelen usar abstracciones como una buena práctica de programación, ya que pueden mejorar la flexibilidad y el mantenimiento del código. Sin embargo, las abstracciones suelen requerir que se ejecute más código. Como se detalla en Cómo reducir la huella de recursos y código de tu app, una base de código compilada más grande aumenta directamente la RAM física que requiere tu app. Si tus abstracciones no proporcionan un beneficio significativo, evítalas.
Usa protobufs lite para datos serializados
Los búferes de protocolo (protobufs) son un mecanismo extensible y neutral en cuanto al lenguaje y la plataforma, diseñado para la serialización de datos estructurados (similar a XML, pero más pequeño, más rápido y más simple). Si usas protobufs para tus datos, siempre usa protobufs lite en tu código del lado del cliente. Los protobufs normales generan código extremadamente detallado, lo que aumenta la huella de código de tu app en la RAM (consulta Administra y optimiza la huella de código de tu app) y contribuye al aumento del tamaño del APK.
Para obtener más información, consulta el archivo readme sobre protobufs.
Ten cuidado con las fugas de memoria
La administración inadecuada de referencias puede provocar fugas de memoria en las que los objetos sobreviven a su vida útil, lo que impide que el recolector de elementos no utilizados recupere la memoria del objeto con fuga. Para evitar fugas de memoria, implementa un diseño que tenga en cuenta el ciclo de vida.
Para obtener más información, consulta Fugas de memoria.
Evita la saturación de la memoria
Los eventos de recolección de elementos no utilizados no afectan el rendimiento de tu app. Sin embargo, muchos eventos de recolección de elementos no utilizados que ocurren en un período breve pueden agotar rápidamente la batería y aumentar de manera marginal el tiempo para configurar fotogramas debido a las interacciones necesarias entre el recolector de elementos no utilizados y los subprocesos de la app. Cuanto más tiempo pase el sistema en la recolección de elementos no utilizados, más rápido se agotará la batería.
A menudo, la saturación de la memoria puede causar una gran cantidad de eventos de recolección de elementos no utilizados. En la práctica, la saturación de la memoria describe la cantidad de objetos temporales asignados que ocurren en un período de tiempo determinado.
Por ejemplo, puedes asignar varios objetos temporales dentro de un bucle for.
También puedes crear objetos Paint o Bitmap nuevos dentro de la función onDraw() de una vista. En ambos casos, la app crea muchos objetos rápidamente y a gran volumen. Estos pueden consumir a gran velocidad toda la memoria disponible de la generación Young y forzar un evento de recolección de elementos no utilizados.
Usa el Generador de perfiles de memoria para encontrar los lugares de tu código donde la saturación de la memoria es alta antes de poder solucionarlos.
Después de que identifiques las áreas problemáticas en el código, intenta reducir el número de asignaciones dentro de las áreas críticas de rendimiento. Considera quitar los elementos de los bucles internos o, quizás, trasladarlos a una estructura de asignación basada en Factory.
También puedes evaluar si los grupos de objetos benefician el caso de uso. Con un grupo de objetos, en lugar de descartar una instancia de objeto en el suelo, la lanzas a un grupo cuando ya no es necesaria. La próxima vez que se necesite una instancia de objeto de ese tipo, podrás adquirirla desde el grupo, en lugar de asignarla.
Evalúa el rendimiento en detalle para determinar si un grupo de objetos es adecuado en una situación determinada. Hay casos en los que los grupos de objetos podrían empeorar el rendimiento. Aunque los grupos evitan las asignaciones, introducen otras sobrecargas. Por ejemplo, mantener el grupo suele implicar una sincronización que tiene una sobrecarga que no es insignificante. Además, borrar la instancia de objeto en grupo para evitar pérdidas de memoria durante el lanzamiento y, luego, su inicialización durante la adquisición podría tener una sobrecarga distinta de cero.
Retener más instancias de objetos en el grupo que lo necesario también genera una carga en la recolección de elementos no utilizados. Si bien los grupos de objetos reducen la cantidad de invocaciones de la recolección de elementos no utilizados, terminan aumentando la cantidad de trabajo necesario para cada invocación, ya que es proporcional a la cantidad de bytes activos (accesibles).