Cómo administrar la memoria de tu app

La memoria de acceso aleatorio (RAM) es un recurso valioso en cualquier entorno de desarrollo de software, pero 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 pérdidas 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 de Reference en el momento apropiado según lo definido por las devoluciones de llamada del ciclo de vida.

En esta página, se explica cómo puedes reducir de manera proactiva el uso de memoria dentro de tu app. Para obtener información sobre cómo el sistema operativo Android administra la memoria, consulta Descripción general de la administración de memoria de Android.

Cómo supervisar la memoria disponible y el uso de memoria

Antes de poder solucionar los problemas de uso de memoria en tu app, debes encontrarlos. En Android Studio, Memory Profiler te ayuda a encontrar y diagnosticar problemas de memoria de las siguientes maneras:

  1. Observa el modo en que tu app asigna memoria en el tiempo. Memory Profiler muestra un gráfico en tiempo real en el que se indica 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.
  2. Inicia eventos de recolección de elementos no utilizados y toma una instantánea del montón de Java mientras se ejecuta tu app.
  3. Registra las asignaciones de memoria de tu app y, luego, 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.

Cómo liberar memoria en respuesta a eventos

Como se describe en Descripción general de la administración de memoria de Android, el sistema operativo puede reclamar memoria de tu app de diferentes maneras o cerrar la app por completo si es necesario a fin de liberar memoria para tareas críticas. Para ayudar a equilibrar aún más la memoria del sistema y evitar la necesidad del sistema de cerrar 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() le permite a tu app detectar eventos relacionados con la memoria en primero o segundo plano y, luego, liberar objetos en respuesta al ciclo de vida de la app o eventos del sistema que indican que el sistema necesita reclamar memoria.

Por ejemplo, puedes implementar la devolución de llamada onTrimMemory() para responder a diferentes eventos relacionados con la memoria, como se muestra aquí:

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 was raised.
         */
        override fun onTrimMemory(level: Int) {

            // Determine which lifecycle or system event was raised.
            when (level) {

                ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                    /*
                       Release any UI objects that currently hold memory.

                       The user interface has moved to the background.
                    */
                }

                ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
                ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
                ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                    /*
                       Release any memory that your app doesn't need to run.

                       The device is running low on memory while the app is running.
                       The event raised indicates the severity of the memory-related event.
                       If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                       begin killing background processes.
                    */
                }

                ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
                ComponentCallbacks2.TRIM_MEMORY_MODERATE,
                ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                    /*
                       Release as much memory as the process can.

                       The app is on the LRU list and the system is running low on memory.
                       The event raised indicates where the app sits within the LRU list.
                       If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                       the first to be terminated.
                    */
                }

                else -> {
                    /*
                      Release any non-critical data structures.

                      The app received an unrecognized memory level value
                      from the system. Treat this as a generic low-memory message.
                    */
                }
            }
        }
    }
    

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 was raised.
         */
        public void onTrimMemory(int level) {

            // Determine which lifecycle or system event was raised.
            switch (level) {

                case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                    /*
                       Release any UI objects that currently hold memory.

                       The user interface has moved to the background.
                    */

                    break;

                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                    /*
                       Release any memory that your app doesn't need to run.

                       The device is running low on memory while the app is running.
                       The event raised indicates the severity of the memory-related event.
                       If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                       begin killing background processes.
                    */

                    break;

                case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
                case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                    /*
                       Release as much memory as the process can.

                       The app is on the LRU list and the system is running low on memory.
                       The event raised indicates where the app sits within the LRU list.
                       If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                       the first to be terminated.
                    */

                    break;

                default:
                    /*
                      Release any non-critical data structures.

                      The app received an unrecognized memory level value
                      from the system. Treat this as a generic low-memory message.
                    */
                    break;
            }
        }
    }
    

Se agregó la devolución de llamada onTrimMemory() en Android 4.0 (API nivel 14). En versiones anteriores, puedes usar onLowMemory(), que equivale aproximadamente al evento TRIM_MEMORY_COMPLETE.

Cómo comprobar cuánta memoria debes usar

A fin de 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 el dispositivo tenga disponible en general. Si tu app alcanzó la capacidad máxima del montón e intenta asignar más memoria, el sistema genera un OutOfMemoryError.

Para evitar quedarte sin memoria, puedes consultar el sistema a fin de determinar cuánto espacio del montón tienes 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 la memoria disponible, la memoria total y el umbral de memoria (el nivel de memoria en el que el sistema comienza a cerrar procesos). El objeto ActivityManager.MemoryInfo también expone un valor booleano simple, lowMemory, 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 to see 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 to see 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;
    }
    

Cómo usar construcciones de código más eficientes en términos de memoria

Algunas funciones de Android, clases de Java y construcciones de código suelen usar más memoria que otras. Puedes minimizar la cantidad de memoria que usa tu app eligiendo alternativas más eficaces en tu código.

Cómo usar los servicios con moderación

Dejar un servicio en funcionamiento cuando no es necesario es uno de los peores errores de administración de memoria que puede cometer una app para Android. Si tu app necesita que un servicio realice el trabajo en segundo plano, no la mantengas activa, a menos que deba ejecutar un trabajo. Recuerda detener el servicio cuando haya completado su tarea. De lo contrario, puedes provocar una pérdida de memoria por error.

Cuando inicias un servicio, el sistema prefiere mantener siempre 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. Eso reduce la cantidad de procesos que el sistema puede mantener 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 el sistema no puede mantener suficientes procesos para alojar a todos los servicios que se ejecutan en ese momento.

En general, debes evitar el uso de servicios persistentes debido a las demandas continuas que realizan a la memoria disponible. En cambio, te recomendamos utilizar una implementación alternativa, como JobScheduler. Si quieres obtener más información sobre cómo usar JobScheduler para programar procesos en segundo plano, consulta Optimizaciones en segundo plano.

Si debes usar un servicio, la mejor manera de limitar la vida útil de este es usar un IntentService, que finaliza tan pronto como termina de procesar el intent que lo inició. Para obtener más información, lee Ejecución en un servicio en segundo plano.

Cómo usar 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 bastante ineficiente en cuanto al uso de memoria porque necesita un objeto de entrada separado para cada asignación.

El marco de trabajo de Android incluye varios contenedores de datos optimizados, incluidos SparseArray, SparseBooleanArray y LongSparseArray. Por ejemplo, las clases SparseArray son más eficientes porque evitan la necesidad del sistema de convertir de manera automática la clave y, a veces, el valor (lo que crea otro objeto o dos por entrada).

Si es necesario, siempre puedes usar arreglos sin formato para una estructura de datos realmente simple.

Ten cuidado con las abstracciones de código

Los desarrolladores suelen usar abstracciones simplemente como una buena práctica de programación, ya que las abstracciones pueden mejorar la flexibilidad y el mantenimiento del código. Sin embargo, las abstracciones tienen un costo significativo. En general, requieren bastante más código que debe ejecutarse, lo que requiere más tiempo y más RAM para que ese código se asigne a la memoria. Por lo tanto, si tus abstracciones no proporcionan un beneficio significativo, debes evitarlas.

Cómo usar protobufs lite para datos serializados

Los búferes de protocolo 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 decides usar protobufs para tus datos, siempre debes usar protobufs lite en tu código del lado del cliente. Los protobufs normales generan código extremadamente detallado, lo que puede causar muchos tipos de problemas en tu app, como un mayor uso de RAM, un aumento significativo del tamaño del APK y una ejecución más lenta.

Para obtener más información, consulta la sección "Versión lite" en el archivo readme sobre protobufs.

Cómo evitar la pérdida de memoria

Como ya mencionamos, en general, 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 corto pueden consumir rápidamente el tiempo de visualización. Cuanto más tiempo pase el sistema en la recolección de elementos no utilizados, menos tiempo tendrá para hacer otras tareas, como renderizar o transmitir audio.

A menudo, la pérdida de memoria puede causar una gran cantidad de eventos de recolección de elementos no utilizados. En la práctica, la pérdida de memoria se describe como 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 nuevos objetos Paint o Bitmap 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.

Por supuesto, debes encontrar los lugares de tu código donde la pérdida de memoria es alta antes de poder solucionarlos. Para eso, debes usar Memory Profiler en Android Studio.

Una vez 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.

Cómo quitar recursos y bibliotecas que requieren mucha memoria

Dentro del código, algunos recursos y bibliotecas pueden consumir mucha memoria sin que te des cuenta. El tamaño general de tu APK, 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.

Cómo reducir el tamaño total del APK

Para reducir en gran medida el uso de la memoria de tu app, puedes reducir el tamaño general de esta. El tamaño del mapa de bits, los recursos, los marcos de animación y las bibliotecas de terceros pueden afectar el tamaño de tu APK. Android Studio y el SDK de Android proporcionan varias herramientas que te ayudarán a reducir el tamaño de los recursos y las dependencias externas. Estas herramientas admiten métodos modernos de reducción de código, como la compilación R8. (En Android Studio 3.3 y versiones anteriores, se usa ProGuard en lugar de la compilación R8).

Para obtener más información sobre cómo reducir el tamaño general de tu APK, consulta la guía sobre cómo reducir el tamaño de tu app.

Cómo usar Dagger 2 para inyección de dependencias

Los marcos de trabajo 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 marco de trabajo de inyección de dependencias en tu app, considera usar Dagger 2. Dagger no usa reflexión para escanear el código de tu app. La implementación estática y en tiempo de compilación de Dagger se puede usar en apps para Android sin costos de tiempo de ejecución ni uso de memoria innecesarios.

Otros marcos de trabajo de inyección de dependencias que utilizan reflexión tienden a inicializar procesos mediante 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.

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 cuando se usa en un cliente móvil. Cuando decidas utilizar una biblioteca externa, es posible 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 decidirte a usarla.

Incluso algunas bibliotecas optimizadas para dispositivos móviles pueden causar problemas debido a las diferentes implementaciones. Por ejemplo, una biblioteca puede 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, marcos de trabajo de carga de imágenes, almacenamiento en caché y muchas otras operaciones que no esperas.

Aunque ProGuard puede ayudar a quitar las API y los recursos con las marcas correctas, no puede quitar las dependencias internas grandes de una biblioteca. Es posible que las funciones que deseas incluir en estas bibliotecas requieran dependencias de menor nivel. Esto se vuelve más problemático cuando usas una subclase de Activity de una biblioteca (que tiende a tener amplias franjas de dependencias), cuando las bibliotecas usan la reflexión (que es común y significa que tienes que pasar mucho tiempo ajustando ProGuard de forma manual para que funcione), y así sucesivamente.

Además, evita usar una biblioteca compartida para una o dos funciones de las docenas de funciones disponibles. No te conviene extraer una gran cantidad de código y sobrecarga que ni siquiera 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.