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 de ciclo de vida.

En esta página, se explica cómo puedes reducir de manera proactiva el uso de memoria dentro de tu app. Si deseas obtener información acerca de la forma en que administra la memoria el sistema operativo Android, consulta la 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, el Generador de perfiles de memoria 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. El Generador de perfiles de memoria 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. Con el objeto de ayudar a equilibrar aún más la memoria del sistema y evitar la necesidad de este de finalizar 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 la app detectar eventos relacionados con la memoria en primer o segundo plano y, luego, liberar objetos en respuesta a su ciclo de vida o a 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 (nivel de API 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 tenga disponible el dispositivo en general. Si tu app alcanzó la capacidad de montón máxima 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 su capacidad disponible, su capacidad total y su umbral (el nivel de memoria en el que el sistema comienza a finalizar 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 aplicación.

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.

Usa 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 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, 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 para usar JobScheduler a fin de programar procesos en segundo plano, consulta Optimizaciones en segundo plano.

Si debes usar un servicio, la mejor manera de limitar su vida útil es usar un IntentService, que finaliza en cuanto 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 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, puedes usar arreglos sin formato para lograr 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 estas pueden mejorar la flexibilidad y el mantenimiento del código. Sin embargo, las abstracciones tienen un costo significativo. En general, requieren que se ejecute bastante más código, 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.

Usa protobufs lite para datos serializados

Los búferes de protocolo (o 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 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 se mencionó anteriormente, los eventos de recolección de elementos no utilizados no afectan el rendimiento de tu app. Sin embargo, muchos de estos eventos 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 aplicación. 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 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 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.

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 el Generador de perfiles de memoria 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.

Otra posibilidad es 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, se puede adquirir desde el grupo, en lugar de asignarla.

La evaluación de rendimiento detallada es esencial para determinar si un grupo de objetos es adecuado en una situación determinada. Hay casos en los que los grupos de objetos pueden 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. Por último, retener más instancias de objetos en el grupo que lo deseado 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 GC, terminan aumentando la cantidad de trabajo que se debe realizar en cada invocación, ya que es proporcional a la cantidad de bytes activos (accesibles).

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 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 total del APK

A fin de reducir significativamente el uso de memoria de tu app, reduce su tamaño general. El tamaño del mapa de bits, los recursos, los marcos de animación y las bibliotecas de terceros pueden contribuir al tamaño de tu app. Android Studio y el SDK de Android proporcionan varias herramientas para ayudarte a reducir el tamaño de tus 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).

Si deseas obtener más información para reducir el tamaño general de tu app, consulta la guía para reducir el tamaño de tu app.

Cómo usar Dagger 2 para inyecció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 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 frameworks 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, frameworks de carga de imágenes, almacenamiento en caché y muchas otras cosas que no esperas.

Aunque ProGuard puede ayudar a quitar las APIs 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 franjas amplias de dependencias), cuando las bibliotecas usan la reflexión (que es común e implica muchos ajustes en 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.