Cómo establecer presupuestos de memoria de la app

Los presupuestos de memoria de las apps permiten que estas declaren un presupuesto de memoria para sí mismas, lo que le indica al sistema que reduzca el uso de memoria de la app cuando esta use más de su presupuesto establecido. Esto es especialmente útil para las apps del sistema y las apps incluidas, o para las apps que se orientan a dispositivos con restricciones de memoria, en los que el desarrollador conoce su conjunto de trabajo de memoria esperado y quiere asegurarse de que su app no use demasiados recursos de RAM compartidos del sistema.

El presupuesto se mantiene equilibrado con la expulsión y el intercambio de memoria para quitar las páginas de memoria que no se usaron recientemente, lo que enfoca la huella de memoria de la app en su conjunto de trabajo actual. Cuando una app supera el presupuesto declarado, el sistema operativo dirige la recuperación específicamente a esa app:

  1. Las páginas respaldadas por archivos limpias (como el código inactivo y los recursos asignados) se descartan primero porque se pueden volver a leer del almacenamiento si es necesario.
  2. Las páginas no sincronizadas respaldadas por archivos se vuelven a escribir en el almacenamiento y se descartan.
  3. Las páginas de memoria anónimas (como las asignaciones de montón) se comprimen y se intercambian a zRAM.

Siempre y cuando el presupuesto no exceda el conjunto de trabajo, la app funcionará bien y no usará más memoria de la que se incluye en su presupuesto establecido. El sistema operativo expulsa la memoria no utilizada y comprime las páginas del montón inactivas para intercambiarlas, de modo que las asignaciones de memoria permanezcan limitadas sin finalizar el proceso.

Cómo declarar presupuestos en el manifiesto de Android

Declarar tus presupuestos de memoria en tu AndroidManifest.xml es el método principal y recomendado para definir presupuestos. No requiere código de tiempo de ejecución, entra en vigencia inmediatamente después del inicio del proceso y proporciona un contrato claro para el sistema operativo.

Cómo declarar un presupuesto de referencia

Para la mayoría de las apps, definir un solo presupuesto para la aplicación es todo lo que se necesita. Declara un elemento <memory-budget> directamente dentro de la etiqueta <application>:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Baseline budget for the application -->
        <memory-budget android:maxMb="256" />

    </application>
</manifest>

Esto establece un presupuesto de memoria residente de 256 MB en todos los procesos y estados del paquete. Cuando el espacio en memoria de la app supera los 256 MB, el sistema operativo recorta las páginas de memoria inactivas con la expulsión y el intercambio.

Cómo variar los presupuestos según el estado del proceso

Una app requiere diferentes cantidades de memoria según su visibilidad para el usuario:

  • Primer plano: El proceso aloja una actividad visible con la que interactúa el usuario. Por lo general, este estado tiene la mayor huella debido a la IU y los gráficos activos.
  • Perceptible: El proceso es perceptible para el usuario, pero no aloja una ventana visible (por ejemplo, alojar un servicio en primer plano de reproducción de medios, navegación paso a paso o un método de entrada activo).
  • Segundo plano: El proceso está ejecutando trabajos en segundo plano, receptores o sincronizaciones de datos. Se espera que mantenga una huella mínima.

Puedes declarar varias cláusulas <memory-budget> para que coincidan con estos estados:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Default budget for visible foreground UI -->
        <memory-budget android:maxMb="200" />

        <!-- Tighter budget when playing audio in background -->
        <memory-budget
            android:maxMb="120"
            android:state="perceptible" />

        <!-- Minimal budget when fully in background -->
        <memory-budget
            android:maxMb="48"
            android:state="background" />

    </application>
</manifest>

No es necesario que especifiques android:state="foreground" en la primera cláusula. Una cláusula sin android:state actúa como resguardo predeterminado para todos los estados. Las cláusulas más restrictivas que se encuentran a continuación anulan el presupuesto cuando la app pasa a los estados perceptible o background.

Apps de varios procesos

Si tu aplicación divide su trabajo en varios procesos, configura presupuestos de procesos dedicados con la etiqueta <process> dentro de <processes>.

Por ejemplo, considera una app de transmisión de música (com.example.radio):

  1. Proceso principal: Aloja la IU visible y el motor de reproducción de audio (MediaSessionService con un servicio en primer plano de mediaPlayback). Cuando está visible, el proceso opera con el presupuesto de primer plano de 180 MB. Cuando el usuario sale de la app mientras la música sigue reproduciéndose, el proceso ingresa al estado perceptible, en el que un presupuesto de 64 MB es suficiente para el motor de reproducción y el búfer de audio.
  2. Proceso de sincronización (:sync): Proceso dedicado que ejecuta la sincronización de metadatos en segundo plano y la indexación de descargas. Dado que este proceso solo está activo en segundo plano, no es necesario que declares state="background" de forma explícita; se aplica un solo presupuesto.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.radio">

    <application
        android:label="@string/app_name">

        <!-- Package baseline: main process with UI and audio playback -->
        <memory-budget android:maxMb="180" />

        <!-- Tighter budget when audio plays in the background -->
        <memory-budget
            android:maxMb="64"
            android:state="perceptible" />

        <!-- Dedicated background sync process -->
        <processes>
            <process android:process=":sync">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <service
            android:name=".playback.AudioPlayerService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="false" />

        <service
            android:name=".sync.PlaylistSyncService"
            android:process=":sync"
            android:exported="false" />

    </application>
</manifest>

El uso de memoria en cualquier subproceso se contabiliza tanto para el presupuesto del proceso como para el presupuesto del paquete que lo contiene. Un proceso experimenta presión de memoria en el tiempo de ejecución si supera su presupuesto de proceso o el presupuesto del paquete, según el umbral que se alcance primero.

Cómo aumentar los presupuestos para pantallas de alta densidad

Para las aplicaciones cuyo espacio en memoria se ajusta significativamente según la cantidad de píxeles que se deben dibujar en la pantalla a la vez (como una app de galería de fotos que almacena en caché mapas de bits dimensionados para la pantalla), Android proporciona dos mecanismos alternativos para ajustar los presupuestos de forma dinámica con las especificaciones de la pantalla:

  • Ajuste según el intervalo de densidad de pantalla (android:additionalMbPerDensity): Agrega megabytes de forma proporcional a la relación de densidad de la pantalla en relación con mdpi (1.0 x / 160 dpi). Esto es adecuado cuando el uso de memoria se ajusta según los intervalos de densidad de la IU, como el almacenamiento en caché de elementos de diseño rasterizados de mayor resolución o recursos de la IU:

    <!-- Baseline 180MB + 16MB per 1.0x density ratio -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    En una pantalla mdpi (1.0x), el presupuesto es de 180 MB + 16 × 1 = 196 MB. En una pantalla xxhdpi (3.0x), el presupuesto se ajusta a 180 MB + 16 × 3 = 228 MB.

  • Ajuste según la resolución de pantalla física (android:additionalBytesPerDisplayPixel): Agrega bytes directamente por píxel de pantalla física (ancho × alto). Esto es ideal para las aplicaciones que asignan superficies gráficas de pantalla completa, búferes de renderización o cachés de fotos de resolución completa en las que el consumo de memoria se ajusta directamente con el recuento de píxeles de pantalla sin procesar en lugar de los discretos de densidad de la IU:

    <!-- Baseline 128MB + 16 bytes per physical display pixel -->
    <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering -->
    <memory-budget
        android:maxMb="128"
        android:additionalBytesPerDisplayPixel="16" />
    

    En una pantalla de 1080p (1080 × 2400 ≈ 2.59 M de píxeles), esto agrega aproximadamente 41.4 MB al presupuesto de referencia. En una pantalla de 1440p (1440 × 3120 ≈ 4.49 M de píxeles), agrega alrededor de 71.8 MB.

Estos dos atributos son alternativas. Elige el atributo que coincida con el factor de ajuste principal de tu aplicación y evita combinar ambos en la misma cláusula.

Especializa el diseño para los factores de forma de los dispositivos

Cuando envíes un APK a teléfonos, tablets y Wear OS, usa el atributo android:feature para ajustar los presupuestos para diferentes objetivos de hardware.

En los relojes con Wear OS, la RAM es limitada, y la IU y el conjunto de funciones de la app son mucho más simples. Puedes declarar un presupuesto más ajustado y especializado para la función watch:

<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />

<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
    android:maxMb="48"
    android:feature="watch" />

Regla de resolución: Entra en vigencia la última cláusula aplicable

Cuando se definen varios elementos <memory-budget> para una aplicación o un proceso, el sistema los evalúa en el orden en que se declaran en el manifiesto. Se aplica la cláusula de presupuesto última aplicable.

Debido a que gana el último presupuesto aplicable, el orden es importante. Siempre coloca primero el presupuesto de referencia más general, seguido de las anulaciones más específicas (como las cláusulas específicas del estado o del hardware).

Referencia de atributos XML

Todos los atributos de tamaño de memoria se expresan en megabytes (MB) y se asignan al cargo memory.current del cgroup de Linux (que excluye la memoria compartida, como Zygote).

Atributo Formato Predeterminado Descripción
android:maxMb Número entero (> 0) Obligatorio Es el límite de presupuesto de memoria residente de referencia en MB.
android:state Enum Cualquiera Estado del proceso al que se aplica este presupuesto: foreground, perceptible o background.
android:additionalMbPerDensity Número entero (≥ 0) 0 Son los megabytes adicionales que se agregan por unidad de relación de densidad de pantalla en relación con mdpi (1.0x).
android:additionalBytesPerDisplayPixel Número entero (≥ 0) 0 Bytes adicionales asignados por píxel de pantalla física (ancho × alto), útiles para los búferes de superficie y los mapas de bits.
android:feature String Cualquiera Restringe la cláusula a los dispositivos que declaran funciones de hardware específicas: watch, automotive o leanback.

APIs de Runtime (opción dinámica secundaria)

Declarar presupuestos de forma estática en AndroidManifest.xml es la solución preferida para casi todas las apps. Sin embargo, para las aplicaciones con cargas de trabajo dinámicas o para la experimentación en el tiempo de ejecución, Android proporciona APIs del SDK y el NDK en tiempo de ejecución como una opción secundaria.

La API de entorno de ejecución te permite hacer lo siguiente:

  • Consultar el uso de memoria actual y los presupuestos efectivos
  • Ajusta dinámicamente tu presupuesto de proceso a la baja.
  • Escucha los eventos de exceso de presupuesto para recortar de forma proactiva las cachés antes de que el sistema operativo active la recuperación directa.

API de Kotlin (MemoryBudgetManager)

El servicio del sistema MemoryBudgetManager está disponible a partir de Android 17 QPR2 (versión del SDK de Android 26Q4, nivel de API 37.2/Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Recupera el servicio

val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)

Uso y presupuestos de las consultas

// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes

// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes

Cómo establecer o borrar presupuestos de forma dinámica

Puedes establecer un presupuesto más ajustado en el tiempo de ejecución para restringir la memoria durante las tareas ligeras o borrarlo cuando finalice la tarea:

// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
    budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
    // Thrown if the budget is <= 0 or exceeds the manifest-declared ceiling
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit
budgetManager.clearProcessBudget()

Cómo detectar devoluciones de llamada de presión por exceso de presupuesto

Las apps pueden registrar un receptor para recibir notificaciones cuando el uso de memoria supera el umbral del presupuesto. Esto permite que la app realice una limpieza proactiva a nivel de la aplicación (como borrar las cachés de mapas de bits en la memoria) antes de que el sistema operativo active la latencia de recuperación directa:

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict caches to release memory
    imageTileCache.evictAll()
}

// Register on the main Looper
budgetManager.registerProcessOverBudgetListener(mainLooper, listener)

// When done (e.g., in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

Prácticas recomendadas para las devoluciones de llamada por exceso de presupuesto:

  • Ser rápidas: Las operaciones de recuperación deben ofrecer alivio inmediato. Los cálculos complejos durante la presión empeoran el rendimiento.
  • Evita las asignaciones: No asignes objetos nuevos ni inicies subprocesos nuevos dentro de la devolución de llamada, ya que esto puede activar la recuperación directa inmediata del sistema operativo.
  • Enfócate en los objetivos de alto rendimiento: Desalojar mapas de bits grandes, búferes de renderización o cerrar archivos asignados a la memoria es mucho más eficaz que liberar muchos objetos pequeños.

API nativa del NDK (<android/memory_budget_manager.h>)

Las apps nativas pueden usar la API del NDK de C que expone libandroid.so.

Configuración de CMake

find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})

Incluye el uso de encabezados y consultas

#include <android/memory_budget_manager.h>

// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();

// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
    // Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
    // No budget is currently active
}

Configura el presupuesto nativo de forma dinámica

// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
    const char* error_msg = AMemoryBudgetManager_resultToString(result);
    // Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}

// Clear the dynamic budget to resume manifest limits
AMemoryBudgetManager_clearProcessBudget();

Supervisa los eventos de presión de la memoria

El NDK proporciona dos formas de supervisar los eventos de memoria:

  1. Observador de alto nivel (AMemoryBudgetManager_Watcher_create): Supervisa eventos en un ALooper con eliminación de rebotes automática.
  2. Descriptor de archivo de bajo nivel: AMemoryBudgetManager_getProcessMemoryPressureFd devuelve un descriptor de archivo nativo que se puede integrar directamente en un bucle de motor epoll personalizado.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
    // High-yield eviction of unused native textures or geometry caches
    purgeNativeTextureCaches();
}

// Register watcher on an ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

Ejemplos de la API de Runtime

En los siguientes ejemplos, se muestra cómo implementar las APIs de tiempo de ejecución en Kotlin y C++.

Ejemplo de Kotlin: Editor de imágenes adaptable

En este ejemplo, se muestra una app de edición de imágenes (com.example.imageeditor) que aumenta de forma dinámica su presupuesto de memoria cuando el usuario abre un lienzo de edición de varias capas y borra el presupuesto dinámico cuando vuelve a la vista de galería de miniaturas. También registra un OnOverBudgetListener para expulsar mapas de bits de vista previa almacenados en caché bajo presión.

package com.example.imageeditor.ui

import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.util.LruCache

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager

    // In-memory cache for rendered preview tiles (32MB limit)
    private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
        override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
    }

    private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
        Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
        previewCache.evictAll()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        budgetManager = getSystemService(MemoryBudgetManager::class.java)
    }

    override fun onStart() {
        super.onStart()
        // Register listener for process-level memory breaches
        budgetManager.registerProcessOverBudgetListener(mainLooper, overBudgetListener)
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     */
    fun enterEditingCanvas() {
        try {
            // Dynamically set budget to 256MB for the editing canvas
            budgetManager.processBudgetBytes = 256L * 1024L * 1024L
            Log.i(TAG, "Dynamic budget applied: 256MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        // Clear dynamic budget; restores the baseline manifest budget
        budgetManager.clearProcessBudget()
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}

Ejemplo de C++ del NDK: Motor 3D nativo

En este ejemplo, se muestra un motor de juegos nativo de C++ que administra los presupuestos de memoria según el nivel de calidad de gráficos activo, con AMemoryBudgetManager_Watcher_create en un ALooper para descargar los mipmaps de texturas cuando se supera el presupuesto.

#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>

#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)

class MemoryGovernor {
public:
    MemoryGovernor() : mWatcher(nullptr) {}

    ~MemoryGovernor() {
        stopMonitoring();
    }

    // Configures process budget based on user graphics quality settings
    bool setQualityBudget(int qualityLevel) {
        int64_t targetBytes = 0;
        switch (qualityLevel) {
            case 0: // Low (budget: 128MB)
                targetBytes = 128LL * 1024 * 1024;
                break;
            case 1: // Medium (budget: 256MB)
                targetBytes = 256LL * 1024 * 1024;
                break;
            case 2: // High (budget: 512MB)
                targetBytes = 512LL * 1024 * 1024;
                break;
            default:
                // Clear dynamic override and restore manifest limit
                AMemoryBudgetManager_clearProcessBudget();
                return true;
        }

        AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
        if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
            LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
            return false;
        }
        return true;
    }

    bool startMonitoring(ALooper* looper) {
        if (!looper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            looper,
            AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
            1000,
            &MemoryGovernor::onPressureEvent,
            this
        );
        return mWatcher != nullptr;
    }

    void stopMonitoring() {
        if (mWatcher) {
            AMemoryBudgetManager_Watcher_destroy(mWatcher);
            mWatcher = nullptr;
        }
    }

    void unloadUnusedTextures() {
        LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
        // Fast, high-yield eviction without allocating memory
    }

private:
    static void onPressureEvent(
        int32_t event_mask,
        const AMemoryBudgetEvents* events,
        void* userdata
    ) {
        auto* governor = static_cast<MemoryGovernor*>(userdata);
        governor->unloadUnusedTextures();
    }

    AMemoryBudgetManagerWatcher* mWatcher;
};