Cómo administrar la memoria de tu app

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 para administrar 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 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. En Android Studio, el Generador de perfiles de memoria 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 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.
  • Inicia eventos de recolección de elementos no utilizados y toma una instantánea el montón de Java mientras se ejecuta tu app.
  • 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

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() provisto le permite a la app detectar eventos relacionados con la memoria cuando la app esté en primer o segundo plano. Luego, libera objetos en respuesta a su ciclo de vida o a eventos del sistema que indican que este necesita reclamar memoria.

Puedes implementar la devolución de llamada onTrimMemory() para responder a diferentes eventos relacionados con la memoria, como se muestra en el siguiente ejemplo:

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) {

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

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

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory 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
                   begins stopping 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 is one of the
                   first to be terminated.
                */
            }

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

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

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

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves 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 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
                   begins stopping 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 is one of the
                   first to be terminated.
                */

                break;

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

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

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;
}

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 usan 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

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. 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, 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 tienen un costo significativamente mayor, ya que, 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. 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 usa 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 puede causar muchos 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 el archivo readme sobre protobufs.

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

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).

Cómo quitar recursos y bibliotecas que requieren mucha memoria

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

Para 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 externas 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.

Si deseas obtener más información para disminuir el tamaño general de tu app, consulta Reduce el tamaño de tu app.

Usa Hilt o Dagger 2 para la inserción de dependencias

Los frameworks de inserció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 inserció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 inserció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.

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.

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 probable 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 tiene franjas amplias de dependencias), cuando las bibliotecas usan la reflexión (que es común y requiere ajustes en ProGuard de forma manual para que funcione).

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.