Cómo establecer presupuestos de memoria de la app

Los presupuestos de memoria de la app permiten que esta establezca un límite flexible para la cantidad de memoria que mantiene residente. Cuando tu app supera su presupuesto, el sistema no la finaliza. En cambio, recupera la memoria que tu app no usó recientemente, por lo que la app conserva su conjunto de trabajo (la memoria que necesita de forma activa para lo que hace el usuario) y devuelve el resto al dispositivo.

Los presupuestos son más útiles cuando conoces el conjunto de trabajo de tu app y quieres mantener su huella bajo control, como en el caso de las apps del sistema y empaquetadas, o para las apps que se orientan a dispositivos con memoria limitada.

En esta página, se abordan los siguientes temas:

Cómo funcionan los presupuestos de memoria

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 el uso de memoria de la app en su conjunto de trabajo actual. Cuando una app supera el presupuesto declarado, el sistema operativo se orienta específicamente a esa app para recuperar recursos:

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

La recuperación hace espacio para nuevas asignaciones y mantiene el uso de memoria de tu app dentro de su presupuesto.

Mientras haya margen entre lo que la app necesita en un momento determinado (su conjunto de trabajo) y el presupuesto establecido, la app seguirá ejecutándose con normalidad. Si una app establece un presupuesto menor que su conjunto de tareas, su rendimiento durante el tiempo de ejecución puede disminuir.

Para obtener información sobre cómo el SO hace un seguimiento de la memoria y la recupera, consulta la guía de arquitectura de memoria:

Cómo declarar presupuestos en el manifiesto de Android

Usa AndroidManifest.xml para declarar tus presupuestos de memoria de estado estable, que son los presupuestos de referencia con los que funciona tu app durante el uso normal. Las declaraciones de manifiesto no requieren código de entorno de ejecución, se aplican cuando se inicia el proceso y cambian los presupuestos entre los estados foreground, perceptible y background. PackageManager.getMemoryBudgets expone declaraciones de presupuesto <application> estáticas. Para probar los presupuestos y lanzarlos con la reversión remota antes de confirmarlos en tu manifiesto, consulta Calibra, prueba y lanza presupuestos.

Las declaraciones <memory-budget> se aplican en dispositivos que ejecutan Android 17 QPR2 (nivel de API 37.2) y versiones posteriores. En las versiones anteriores de Android, el analizador de manifiestos de la plataforma ignora los elementos XML no reconocidos, por lo que puedes adoptar <memory-budget> sin afectar la retrocompatibilidad.

Cómo configurar tu compilación

No es necesario que aumentes minSdk ni targetSdk para adoptar presupuestos de memoria de la app. Declarar presupuestos por proceso dentro de <processes> requiere el complemento de Android para Gradle 9.5.0 o una versión posterior. Para compilar los atributos del manifiesto <memory-budget>, MemoryBudgetManager y Build.VERSION_CODES_FULL.CINNAMON_BUN_2, establece compileSdk en el nivel de API 37 con minorApiLevel = 2 en el archivo build.gradle.kts o build.gradle de tu módulo:

Kotlin

android {
    compileSdk {
        version = release(37) {
            minorApiLevel = 2
        }
    }
}

Groovy

android {
    compileSdk {
        version = release(37) {
            it.minorApiLevel = 2
        }
    }
}

Declarar un presupuesto de referencia

En la mayoría de las apps, solo es necesario definir un único presupuesto para la aplicación. 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.

Varía 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 que interactúa con 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, aloja un servicio en primer plano de reproducción de contenido multimedia, una descarga activa en segundo plano, una navegación paso a paso o un método de entrada activo).
  • Segundo plano: El proceso ejecuta trabajos, receptores o sincronizaciones de datos en segundo plano. 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 se requiere una cláusula de primer plano o de estado omitido. Si solo declaras cláusulas perceptible o background, los estados de mayor importancia, como foreground, no se verán limitados por el presupuesto de la app. Si se omite android:state, el valor predeterminado es foreground, que se aplica en cascada a perceptible y background, a menos que una cláusula posterior anule esos estados. Para obtener detalles sobre el orden de las cláusulas, consulta Regla de resolución: Se aplica la última cláusula aplicable.

Cuando establezcas el tamaño de los presupuestos para cada estado, apunta al conjunto de trabajo activo de ese estado, en lugar de la memoria residente máxima (marca de agua alta de RSS):

  • Asigna un tamaño a los presupuestos de foreground para una renderización de fotogramas fluida: Otorga a la IU en primer plano suficiente espacio para su conjunto de trabajo activo, de modo que la recuperación síncrona no retrase la renderización de fotogramas. Si tu app incluye un flujo de trabajo independiente que consume mucha memoria (como un editor de imágenes o videos de alta resolución), considera mover esa actividad a un proceso dedicado (consulta Apps multiproceso) para que sus asignaciones se presupuesten de forma independiente y se eliminen cuando el usuario salga.
  • Ajusta los presupuestos de tamaño de background para el trabajo sin interfaz gráfica: El RSS alto en segundo plano en la telemetría de campo suele reflejar las asignaciones de IU retenidas después de salir del primer plano (consulta Coloca un servicio vinculado con una IU que use mucha memoria y Libera memoria en respuesta a eventos) o las lecturas de archivos únicas durante la indexación y las sincronizaciones en segundo plano. Debido a que el trabajo en segundo plano sin interfaz gráfica no tiene plazos de fotogramas, expulsar las páginas de caché de archivos de un solo uso y cambiar las páginas de montón inactivas a zRAM cuando una sincronización en segundo plano asigna brevemente más de su presupuesto no causa bloqueos visibles para el usuario, a la vez que evita que los picos en segundo plano expulsen la app en primer plano del usuario.

Para obtener un flujo de trabajo paso a paso, consulta Calibra tus presupuestos.

Cómo se asignan los estados del presupuesto a los estados del proceso

La plataforma asigna los estados de presupuesto del manifiesto a los estados del proceso de tiempo de ejecución según RunningAppProcessInfo.importance, que Activity Manager calcula a partir de los componentes activos del proceso y de cualquier vinculación de servicio entrante o consulta del proveedor de contenido de otras apps:

  • foreground: Son interacciones activas y visibles del usuario, como alojar una actividad reanudada, mantener el estado de la app principal cuando se apaga la pantalla o alojar un servicio o proveedor de contenido vinculado o consultado de forma activa por la app principal en primer plano.
  • perceptible: Son cargas de trabajo que el usuario puede percibir sin una ventana visible, como la reproducción activa de contenido multimedia, la navegación paso a paso, la captura de la cámara o el micrófono, las descargas activas en segundo plano, los servicios en primer plano de sincronización de datos o los servicios vinculados por un cliente en primer plano que usa marcas como BIND_NOT_FOREGROUND (consulta Cómo controlar la herencia con marcas BIND). Si bien las métricas internas de la plataforma pueden etiquetar algunas de estas cargas de trabajo como PROCESS_STATE_IMPORTANT_FOREGROUND, esa constante interna no significa una ventana visible y se rige por el presupuesto de perceptible.
  • background: Es el trabajo que no es perceptible de inmediato para el usuario, como los trabajos en segundo plano, las alarmas y los receptores de transmisiones.

En la siguiente tabla, se muestra cómo se asignan los niveles de importancia del entorno de ejecución a los estados del manifiesto:

Manifiesto android:state Importancia del entorno de ejecución (RunningAppProcessInfo) Componentes típicos
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Actividad visible reanudada, app principal con el bloqueo de pantalla, servicio o proveedor de contenido vinculado por la app principal
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Reproducción de contenido multimedia, navegación, descargas o servicios en primer plano de sincronización activos; servicio vinculado con marcas de servicio en primer plano o visibles
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
IMPORTANCE_CACHED
Trabajos en segundo plano, receptores, sincronización en segundo plano y procesos almacenados en caché

Para obtener detalles sobre cómo las vinculaciones de clientes elevan un proceso de host de servicio o proveedor y cómo el proceso vuelve a background o cached después de que el cliente se desvincula o se mueve fuera de la pantalla, consulta Vinculaciones de servicios y estados de procesos.

Cuando un proceso entra en un estado almacenado en caché (IMPORTANCE_CACHED) sin componentes activos, la plataforma sigue aplicando su presupuesto de memoria de background junto con la congelación y la recuperación de procesos almacenados en caché, lo que mantiene el proceso limitado incluso si no se produce la congelación o la compactación.

Inspecciona el estado del proceso y el presupuesto de tu app

Una forma de inspeccionar el estado y la importancia del proceso activo de tu app durante el desarrollo es consultar Activity Manager con ADB:

adb shell dumpsys activity processes <package-name>

En el siguiente ejemplo abreviado, se muestran el registro del proceso y las entradas de control de OOM para una app que ejecuta un servicio en primer plano:

ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
  All known processes:
  *APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
    pid=3919
    oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
    curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
    hasStartedServices=true
    mHasForegroundServices=true forcingToImportant=null
...
  Process OOM control (48 total):
    Proc #22: prcp  F/S/FGS  ---NFU-TI  t: 0 3919:com.example.app/u0a123 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS  lastRss=0.00 lastCachedRss=0.00

En este resultado, se ilustra lo siguiente:

  • curProcState=4 y state: cur=FGS reflejan que el proceso se encuentra en el estado de servicio en primer plano. Si tu app aloja una actividad visible activa, aparecerá como TOP. Si ejecuta un componente importante en primer plano (como una descarga o una sincronización), aparece como IMPF.
  • En la tabla de control de OOM de procesos, prcp indica que el proceso se evalúa en el nivel de prioridad perceptible, que corresponde al presupuesto de perceptible.

Para inspeccionar el uso de memoria residente actual de tu app, haz lo siguiente:

adb shell dumpsys meminfo <package-name>

Apps con 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, imagina 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 funciona con el presupuesto de primer plano de 180 MB. Cuando el usuario sale de la app mientras se sigue reproduciendo música, el proceso entra en el 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): Es un proceso dedicado que ejecuta la sincronización de metadatos en segundo plano y la indexación de descargas. Como 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" />

        <processes>
            <!-- Dedicated background sync process -->
            <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>

Cuando declares un bloque <processes> en tu manifiesto, incluye entradas <process> solo para los procesos que declaren un <memory-budget>. Si quieres asignar un presupuesto solo a subprocesos específicos y dejar el resto de la app en el límite del sistema para el dispositivo, puedes omitir los elementos <memory-budget> directamente en <application>.

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

Ajusta los presupuestos para las pantallas de alta densidad

En el caso de las apps cuyo espacio en memoria se escala de forma significativa según la cantidad de píxeles que se deben dibujar a la vez, como una app de galería de fotos que almacena en caché mapas de bits con el tamaño de la ventana, Android proporciona dos mecanismos alternativos para escalar los presupuestos de forma dinámica con las especificaciones de la pantalla:

  • Escala por bucket de densidad de visualización (android:additionalMbPerDensity): Agrega megabytes de forma proporcional al cuadrado de la proporción de densidad de la pantalla en relación con mdpi (1.0x/160 dpi), ya que los elementos de diseño de trama 2D y los recursos de IU se escalan tanto en el ancho como en la altura:

    <!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    En una pantalla mdpi (1.0x), el presupuesto es de 180 + 16 × 12 = 196 MB. En una pantalla xxhdpi (3.0x), el presupuesto se escala a 180 + 16 × 32 = 324 MB.

  • Escala por recuento de píxeles de la ventana (android:additionalBytesPerDisplayPixel): Agrega bytes por píxel en los límites de la ventana activa (WindowMetrics.getBounds ancho × alto). Esto se adapta a las apps que asignan superficies de gráficos de ventana completa, búferes de renderización o cachés de fotos de alta resolución en las que el consumo de memoria se escala directamente con el recuento de píxeles sin procesar, en lugar de los buckets de densidad de la IU:

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

    En una ventana de 1080p de pantalla completa (1080 × 2400 y &approx; 2.59 millones de píxeles), esto agrega 41,472,000 bytes (&approx; 39.6 MB, donde 1 MB = 1,048,576 bytes) al presupuesto de referencia. En una ventana de 1440p (1440 × 3120 &approx; 4.49 M de píxeles), agrega 71,884,800 bytes (&approx; 68.6 MB).

Estos dos atributos son alternativos. Elige el atributo que coincida con el factor de escala principal de tu app y evita combinar ambos en la misma cláusula.

Especializa tu app para factores de forma de dispositivos

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

En los relojes Wear OS, la RAM está limitada, y la IU y el conjunto de funciones de la app son mucho más sencillos. Puedes declarar un presupuesto más ajustado 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: Se aplica la última cláusula aplicable

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

Un android:state omitido se establece de forma predeterminada en foreground, que también se aplica en cascada a perceptible y background, a menos que se anule con una cláusula posterior para esos estados. Ordena las cláusulas de foreground (o android:state omitido) a perceptible a background, y coloca las anulaciones de android:feature dentro del grupo de estados coincidente para que una anulación de estado omitido final no oculte las cláusulas perceptible o background anteriores.

Referencia de atributos XML

Todos los atributos de megabytes (MB) usan unidades binarias (1 MB = 1,048,576 bytes) y se asignan al cargo memory.current de cgroup de Linux (que excluye la memoria compartida de 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 foreground El estado del proceso al que se aplica este presupuesto: foreground, perceptible o background. Si se omite, el valor predeterminado es foreground y se aplica en cascada a perceptible y background, a menos que se anule con una cláusula posterior.
android:additionalMbPerDensity Número entero (≥ 0) 0 Megabytes adicionales multiplicados por el cuadrado de la proporción de densidad de la pantalla en relación con mdpi (1.0x).
android:additionalBytesPerDisplayPixel Número entero (≥ 0) 0 Son los bytes adicionales asignados por píxel de ventana (WindowMetrics.getBounds ancho × alto), útiles para búferes de superficie y 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 entorno de ejecución

Junto con las cláusulas declarativas del manifiesto <memory-budget>, Android proporciona una API del SDK del entorno de ejecución para las siguientes tareas:

  • Valida los presupuestos candidatos detrás de la configuración remota antes de confirmarlos en AndroidManifest.xml, como se describe en Lanza presupuestos en etapas.
  • Consulta el uso de memoria de cgroup en vivo y los presupuestos efectivos en proceso.
  • Ajusta de forma dinámica el presupuesto de un proceso o paquete durante las tareas ligeras.
  • Escucha los eventos de exceso de presupuesto para recortar las cachés cuando el sistema operativo recupere memoria.

API del SDK de Android (MemoryBudgetManager)

El servicio del sistema MemoryBudgetManager está disponible para las apps escritas en Kotlin y Java a partir de Android 17 QPR2 (versión secundaria del SDK, nivel de API 37.2/Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Recupera el servicio

Antes de acceder a MemoryBudgetManager, verifica que el dispositivo ejecute Android 17 QPR2 o una versión posterior. Para ello, consulta SDK_INT_FULL. Si el minSdk de tu app es inferior a 36, primero verifica SDK_INT para que los dispositivos que ejecutan Android 15 (nivel de API 35) o versiones anteriores no arrojen NoSuchFieldError cuando lean SDK_INT_FULL:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
    Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2
) {
    val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}

Uso de consultas y presupuestos

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

// Query process-scope and package-scope limits (LIMIT_IS_DISABLED if unset)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes

Establece o borra presupuestos de forma dinámica

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

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

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

A diferencia de las cláusulas del manifiesto <memory-budget>, que especifican límites independientes para los estados foreground, perceptible y background, un presupuesto dinámico establecido a través de MemoryBudgetManager aplica un único límite de tiempo de ejecución hasta que el código lo actualice o lo borre. Si usas la API de tiempo de ejecución para ejecutar experimentos de campo o administrar límites específicos del estado, actualiza o borra el presupuesto dinámico en las transiciones del ciclo de vida, como a través de ProcessLifecycleOwner o devoluciones de llamada del ciclo de vida de la actividad, como se muestra en el ejemplo de editor de imágenes adaptable.

Rendimiento e idempotencia

Cuando trabajes con MemoryBudgetManager en el entorno de ejecución, ten en cuenta las siguientes características:

  • Idempotencia: Todas las APIs de mutación de presupuesto son idempotentes. Llamar a clearProcessBudget() o clearPackageBudget() de forma repetida o cuando no hay un presupuesto dinámico activo es una operación segura que no tiene efecto. Del mismo modo, establecer setProcessBudgetBytes() o setPackageBudgetBytes() en el mismo valor de forma consecutiva no activa reconfiguraciones redundantes del sistema.
  • Frecuencia de llamada: Cuando se establece, actualiza o borra un presupuesto, se realiza una llamada entre procesos al servicio del sistema para actualizar el controlador de memoria del sistema operativo del proceso. Si bien es ligero, no es completamente gratuito. Llama a estas APIs durante las transiciones discretas del ciclo de vida y los hitos de las tareas (como ingresar o salir de una actividad, iniciar o completar el proceso en segundo plano, o controlar las solicitudes de voz), y evita llamarlas en bucles cerrados, subprocesos de procesamiento de audio o rutinas de renderización por fotograma.
  • Frecuencia de consultas: Las propiedades de consulta processCurrentUsageBytes, packageCurrentUsageBytes, processBudgetBytes y packageBudgetBytes leen los límites y el uso de cgroup en vivo en proceso a través de llamadas nativas locales en lugar de llamar al servicio del sistema. Aunque estas consultas son rápidas, invócalas a pedido (como durante las transiciones de tareas o dentro de las devoluciones de llamada de OnOverBudgetListener) en lugar de realizar sondeos en bucles cerrados.

Escucha las devoluciones de llamada de presión por exceso de presupuesto

Las apps pueden registrar un objeto de escucha para recibir notificaciones cuando el uso de memoria supere el umbral del presupuesto. Esto permite que la app realice una limpieza proactiva a nivel de la app (como borrar las cachés de mapas de bits en la memoria) mientras el sistema operativo recupera memoria para mantener el proceso o el paquete dentro de su presupuesto:

val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict thread-safe caches on the background Looper
    imageTileCache.evictAll()
}

// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)

// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

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

  • Usa un fondo Looper: Pasa un fondo HandlerThread Looper a registerProcessOverBudgetListener o registerPackageOverBudgetListener siempre que sea posible. Ejecutar la expulsión de la caché o la limpieza de archivos en un subproceso en segundo plano evita competir con la renderización de fotogramas en el subproceso de IU mientras el proceso está bajo presión de memoria. Reserva Looper.getMainLooper para los objetos de escucha que solo tocan el estado de la IU vinculado al subproceso principal.
  • Espera devoluciones de llamada repetidas: Las devoluciones de llamada por exceso de presupuesto no son notificaciones de un solo intento. Cuando el uso supera el presupuesto, el sistema operativo reclama primero las páginas inactivas y, si la presión continúa, el sistema sigue reclamando memoria y, como máximo, invoca tu objeto de escucha una vez por segundo. Haz que tu devolución de llamada sea idempotente. Si tu app solo necesita una notificación o quiere un tiempo de espera más largo entre las pasadas de limpieza, llama a unregisterProcessOverBudgetListener (o unregisterPackageOverBudgetListener) dentro de la devolución de llamada o protege la limpieza con una marca de tiempo de espera o AtomicBoolean.
  • Regístrate antes de ajustar un presupuesto: Si el uso de memoria actual ya supera un presupuesto nuevo cuando llamas a setProcessBudgetBytes o setPackageBudgetBytes, se activa un evento de exceso de presupuesto en cuanto se aplica el presupuesto nuevo. Registra tu objeto de escucha antes de establecer el presupuesto para que tu app no se pierda esa notificación inicial de exceso de presupuesto.
  • Rapidez: Las operaciones de recuperación deben ofrecer un 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, si lo haces, se puede activar la recuperación directa inmediata del sistema operativo.
  • Enfócate en los objetivos de alto rendimiento: Expulsar mapas de bits grandes, renderizar búferes o cerrar archivos asignados a la memoria es mucho más eficaz que liberar muchos objetos pequeños.

Ejemplo de la API de Runtime

En el siguiente ejemplo, se muestra cómo implementar la API de entorno de ejecución con la API del SDK de Android en Kotlin:

Ejemplo del SDK de Android: Editor de imágenes adaptable

En este ejemplo, se muestra una app de edición de imágenes (com.example.imageeditor) cuyo manifiesto declara un límite de 256 MB para alojar su lienzo de varias capas:

<manifest ... >
    <application ... >
        <!-- Manifest ceiling accommodates the heaviest editing workload -->
        <memory-budget android:maxMb="256" />
    </application>
</manifest>

Cuando el usuario explora la galería de miniaturas livianas, la app usa la API del SDK de Android en Kotlin para ajustar de forma dinámica su presupuesto de proceso a 96 MB. Cuando el usuario abre el lienzo de edición de varias capas, la app borra el presupuesto dinámico para restablecer el límite completo de 256 MB del manifiesto. También registra un OnOverBudgetListener en un HandlerThread de Looper en segundo plano antes de aplicar el presupuesto de la galería para expulsar los mapas de bits de vista previa almacenados en caché bajo presión sin bloquear el subproceso de IU.

package com.example.imageeditor.ui

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

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager
    private lateinit var cleanupThread: HandlerThread

    // Thread-safe 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)
        cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
    }

    override fun onStart() {
        super.onStart()
        // Register listener on a background Looper before tightening the budget
        budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
        // Clear the activity-scoped gallery budget when leaving the activity.
        budgetManager.clearProcessBudget()
    }

    override fun onDestroy() {
        cleanupThread.quitSafely()
        super.onDestroy()
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     * Clears the dynamic budget, restoring the full 256MB manifest ceiling.
     */
    fun enterEditingCanvas() {
        // Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
        budgetManager.clearProcessBudget()
        Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     * Re-applies the tighter dynamic budget.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        applyGalleryBudget()
    }

    private fun applyGalleryBudget() {
        try {
            // Dynamically tighten budget to 96MB for the lightweight gallery view
            budgetManager.processBudgetBytes = 96L * 1024L * 1024L
            Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

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

Calibra, prueba y libera presupuestos

Para implementar correctamente un presupuesto de memoria, se deben seguir tres pasos:

  1. Calibra el presupuesto para cada estado del proceso.
  2. Realiza pruebas exhaustivas antes de lanzarla.
  3. Lánzala por etapas: Valídala detrás de la configuración remota, incorpórala a tu manifiesto y sigue midiendo.

Calibra tus presupuestos

Un buen presupuesto se encuentra justo por encima del conjunto de trabajo de tu app para un estado determinado, no en su punto máximo sin restricciones. Para encontrar ese punto, ejecuta un flujo de usuarios realista, disminuye el presupuesto paso a paso y observa el momento en que tu app comienza a pagar por él.

Información sobre el conjunto de trabajo y la memoria residente

Sin un presupuesto, la huella residente de tu app (memory.current) contiene dos tipos de memoria:

  • El conjunto de trabajo: el montón de Java, Kotlin y nativo, y las páginas respaldadas por archivos activos (código DEX, bibliotecas nativas y recursos) que el flujo de usuarios actual toca una y otra vez
  • Inactividad en frío: cachés de mapas de bits de pantallas anteriores, búferes de inicio de una sola vez, inactividad del montón que queda después de una ráfaga de asignación y páginas de archivos que se leyeron una vez y no se volvieron a tocar

Un presupuesto en tu punto máximo sin restricciones nunca recupera la holgura fría. Un presupuesto inferior a tu conjunto de trabajo hace que el kernel intercambie páginas del montón y expulse páginas de código que tu app vuelve a fallar de inmediato. Estas fallas de reingreso del conjunto de trabajo consumen tiempo de CPU y batería, y, una vez que llegan al subproceso de IU, también consumen fotogramas.

Lo que buscas es el punto de inflexión: el presupuesto más bajo en el que la recuperación quita solo la inactividad fría, por lo que los errores de página, el tiempo de CPU y las latencias de fotogramas se mantienen donde estaban sin un presupuesto.

Prepara tu entorno de pruebas

Configura una prueba repetible que se comporte como la producción:

  • Calibra la misma compilación de lanzamiento optimizada para R8 y no depurable que envías. Las compilaciones de depuración usan más memoria, por lo que un presupuesto ajustado en una compilación de depuración es demasiado alto para el lanzamiento.
  • Realiza pruebas en un dispositivo o emulador que ejecute Android 17 QPR2 (nivel de API 37.2) o una versión posterior. Una compilación userdebug o una imagen de emulador sin Google Play es conveniente cuando se toman muestras de archivos cgroup de adb shell, ya que leer el directorio cgroup de una app desde la shell puede requerir adb root. Una app puede leer el uso de cgroup de su propio proceso en curso a través de MemoryBudgetManager, incluso en las compilaciones de producción.
  • Crea una secuencia de comandos de un flujo de usuarios de 30 a 60 segundos con Jetpack Macrobenchmark, UI Automator o adb shell input, una vez para cada estado que planees presupuestar. Una app inactiva no te dice nada sobre su conjunto de trabajo.
    • foreground: Desplázate por listas densas y cuadrículas de contenido multimedia, muévete entre tus destinos principales, abre menús y diálogos, y ejecuta animaciones.
    • perceptible: Deja la ventana visible mientras continúa el trabajo perceptible (por ejemplo, la reproducción de audio o una descarga), deja que se ejecute durante 10 a 15 segundos y, luego, vuelve a la app.
    • background: Activa una sincronización, un trabajo de WorkManager (por ejemplo, con adb shell cmd jobscheduler run -f <package-name> <job-id>), un servicio iniciado o un receptor de transmisiones mientras la app no tiene una IU visible.

Mide un presupuesto de background mientras se ejecuta activamente el trabajo en segundo plano, en lugar de hacerlo mientras el proceso está inactivo en un estado almacenado en caché (IMPORTANCE_CACHED). Aunque el presupuesto de background permanece vigente mientras se almacena en caché, un proceso inactivo almacenado en caché no ejecuta trabajo en segundo plano y no refleja el conjunto de trabajo de tus trabajos o receptores en segundo plano.

Mide tu punto de referencia

Comienza con ejecuciones que no tengan ningún presupuesto para que sepas cómo se ve una ejecución normal. Usa una compilación sin declaraciones <memory-budget> y, luego, inicia cada ejecución desde un proceso nuevo (adb shell am force-stop <package-name>).

Cada proceso de la app tiene su propio controlador de memoria de cgroup v2. Busca su ruta de acceso en la línea 0:: de /proc/<PID>/cgroup en lugar de codificarla de forma rígida. Pasa cada comando del dispositivo a adb shell como una sola cadena entre comillas para que la shell del dispositivo vea las comillas:

PID=$(adb shell pidof -s <package-name>)
CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")
adb shell "cat $CG/memory.current $CG/memory.swap.current"
adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

La lectura de estos archivos es económica y no interrumpe tu app, por lo que puedes muestrearlos cada segundo durante una ejecución:

  • memory.current: Es la memoria residente que se cobra al proceso, en bytes. Este es el número que limita <memory-budget>.
  • memory.swap.current: Es la memoria comprimida en el espacio de intercambio de zRAM, en bytes.
  • anon y file en memory.stat: Memoria residente anónima y respaldada por archivos, en bytes
  • workingset_refault_anon y workingset_refault_file en memory.stat: Son recuentos continuos de páginas intercambiadas o expulsadas que tu app volvió a necesitar. Estos contadores son el principal indicador de que un presupuesto es demasiado ajustado.

Si recibes un error de permiso cuando lees los archivos cgroup de una app desde adb shell, ejecuta adb root en una compilación userdebug o una imagen de emulador sin Google Play y, luego, vuelve a intentarlo. En las compilaciones de producción de user en las que adb root no está disponible, consulta MemoryBudgetManager.processCurrentUsageBytes desde tu app o usa adb shell dumpsys meminfo <package-name> para obtener una vista más general. En App Summary, Java Heap, Native Heap y Stack son, en su mayoría, memoria anónima, y Code está respaldada por archivos. Los mapas de bits de software se consideran para Native Heap y tu presupuesto, mientras que los mapas de bits de hardware y Graphics no. No ejecutes dumpsys meminfo en un bucle mientras mides la latencia de fotogramas. Fuerza una recolección de elementos no utilizados en tu app y recorre todas sus asignaciones de memoria, lo que cambia lo que intentas medir.

Presupuestos de candidatos de barrido

Puedes probar los presupuestos candidatos sin volver a compilar tu app. Usa am memory-limiter manual para establecer un presupuesto para un proceso en ejecución y none para quitarlo:

adb shell am memory-limiter manual <PID> 120MB
adb shell am memory-limiter manual <PID> none

La anulación manual dura todo el ciclo de vida de esa instancia del proceso, en todos los cambios de estado del proceso, por lo que debes borrarla o reiniciar el proceso antes de continuar. También establece el límite estricto del proceso (memory.max) en el mismo valor, por lo que un presupuesto candidato por debajo del conjunto de trabajo de tu app puede hacer que se finalice el proceso en lugar de solo ralentizarlo. Trata eso como un presupuesto fallido.

También puedes realizar un barrido desde una compilación de comparativas leyendo un valor candidato de un argumento de instrumentación y configurando MemoryBudgetManager.processBudgetBytes. Un presupuesto de tiempo de ejecución solo puede ajustar un presupuesto de manifiesto, por lo que debes realizar un barrido en una compilación sin declaraciones <memory-budget>. También permanece vigente en todos los cambios de estado del proceso hasta que llamas a clearProcessBudget.

Comienza con el memory.current más alto de tu modelo de referencia y disminuye de 10 a 15 MB por vez. Para cada presupuesto candidato, haz lo siguiente:

  1. Inicia un proceso nuevo, llévalo al estado objetivo y aplica el presupuesto.
  2. Ejecuta el flujo de usuarios una vez como preparación. Aplicar un presupuesto expulsa las páginas de inicio en frío una vez, y tú quieres medir el estado estable en lugar de esa transición.
  3. Restablece las estadísticas de fotogramas y registra los contadores de fallas de página de inicio y el tiempo de CPU del proceso (utime y stime, en ciclos de reloj):

    adb shell dumpsys gfxinfo <package-name> reset
    adb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"
    adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"
  4. Ejecuta el pase medido de tu flujo de usuarios.

  5. Vuelve a registrar memory.current, anon, file, los contadores de refault y el tiempo de CPU, y recopila las estadísticas de fotogramas. dumpsys gfxinfo puede imprimir las mismas estadísticas más de una vez, así que toma la primera coincidencia, que abarca toda la app:

    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'
    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'

Repite cada candidato al menos tres veces y, luego, intercala los candidatos en lugar de ejecutar todas las repeticiones de un candidato de forma consecutiva. Compara cada resultado con la dispersión de tus ejecuciones de referencia, no con una sola ejecución.

Lee los resultados

A medida que disminuye el presupuesto, tu app pasa por tres regímenes:

  • Por encima de la rodilla: memory.current sigue el presupuesto y descarta las páginas file frías y el montón inactivo. anon se mantiene estable, los errores de referencia se mantienen cerca de la referencia, y el tiempo de CPU y las latencias de fotogramas no se mueven. Aquí es donde quieres estar.
  • Debajo de la rodilla: Las páginas inactivas desaparecieron, por lo que el kernel comienza a intercambiar páginas del montón que tu app aún usa. Los errores de página aumentan en cientos o miles de páginas por ejecución, y el tiempo de CPU aumenta. Es posible que la latencia de fotogramas apenas se mueva en un dispositivo de prueba que, de lo contrario, estaría inactivo, pero tu app ahora dedica tiempo de CPU y batería a comprimir y descomprimir su propio conjunto de trabajo.
  • Muy por debajo del punto de inflexión: Las fallas de recuperación y de intercambio se producen en el subproceso de IU y en RenderThread, y aumentan los bloqueos, la latencia de fotogramas y los tiempos de reanudación en caliente.

En comparación con tu modelo de referencia sin restricciones, un presupuesto candidato se aprueba si cumple con todos los siguientes requisitos, que son un punto de partida razonable:

  • El proceso sobrevive a la ejecución.
  • Los deltas de fallas de página repetidas se mantienen cerca de la referencia
  • El tiempo de CPU del proceso no aumenta más de un 5%.
  • Los fotogramas con bloqueos no aumentan más de 1 punto porcentual.
  • La latencia de fotogramas del P95 no aumenta más de 3 ms.
  • El tiempo de reanudación en caliente (TotalTime de adb shell am start -W) aumenta en no más del 15% para los presupuestos de perceptible y background

El punto de inflexión es el presupuesto candidato más bajo que se aprueba. Por ejemplo, aquí tienes un barrido en primer plano para una app hipotética con un pico sin restricciones de 138 MB:

Presupuesto candidato memory.current anon file Refaults (páginas) Latencia de fotogramas P95 Regime
Ninguno 138 MB 75 MB 63 MB 0 10.5 ms Modelo de referencia
125 MB 121 MB 74 MB 47 MB 10 10.5 ms Por sobre la rodilla
110 MB 110 MB 75 MB 35 MB 15 10.8 ms Rodilla
95 MB 95 MB 74 MB 21 MB 176 11.2 ms Por debajo de la rodilla
85 MB 85 MB 68 MB 17 MB 2,050 12.8 ms Por debajo de la rodilla
70 MB 70 MB 54 MB 16 MB 8,771 16.5 ms Muy por debajo de la rodilla

Elige tu presupuesto

Establece el android:maxMb entre un 10% y un 20% por encima del punto de inflexión, de modo que las diferencias en los datos del usuario, las configuraciones regionales y las ráfagas de asignación no hagan que tu app quede por debajo de ese valor. En el ejemplo anterior, el punto de inflexión es de 110 MB, por lo que 125 MB es una buena opción.

Luego, instala una compilación de lanzamiento con las nuevas declaraciones de <memory-budget>, vuelve a ejecutar el flujo de usuarios y verifica que se sigan cumpliendo los criterios de aprobación. Para confirmar el presupuesto activo en el entorno de ejecución, consulta MemoryBudgetManager.processBudgetBytes o MemoryBudgetManager.packageBudgetBytes.

Valida los presupuestos que no están en primer plano con un retorno en caliente

Un presupuesto ajustado de perceptible o background puede parecer correcto mientras tu app está en ese estado y, luego, costarte cuando el usuario regrese. Prueba también el viaje de regreso:

  1. Ejecuta el flujo en primer plano, mueve la app al estado de destino y aplica el presupuesto candidato con am memory-limiter manual.
  2. Deja que el trabajo perceptible o en segundo plano se ejecute durante 10 a 15 segundos.
  3. Borra la anulación (adb shell am memory-limiter manual <PID> none) y, luego, vuelve a abrir la app de inmediato con adb shell am start -W y registra TotalTime. Si, en cambio, configuras el candidato a través de MemoryBudgetManager, llama a clearProcessBudget antes de que la app devuelva un valor.
  4. Ejecuta una interacción breve en primer plano y verifica los errores de página repetidos y las estadísticas de fotogramas con respecto a los criterios de aprobación.

Elige el presupuesto más bajo que apruebe ambas pruebas mientras la app se encuentra en ese estado y en el retorno en caliente, y agrega el mismo margen del 10% al 20%.

Calibra el ajuste de escala de la pantalla

Si usas android:additionalBytesPerDisplayPixel, busca el punto de inflexión en primer plano en dos pantallas con resoluciones diferentes, como 1080 × 2400 (aproximadamente 2.59 millones de píxeles) y 1440 × 3120 (aproximadamente 4.49 millones de píxeles). Divide la diferencia entre los dos puntos de inflexión, en bytes, por la diferencia en el recuento de píxeles para obtener android:additionalBytesPerDisplayPixel. Resta esa cantidad por píxel para la pantalla más pequeña de su punto de inflexión para obtener la base independiente de la resolución y agrega margen para obtener android:maxMb.

Prueba tus presupuestos

Antes de lanzar un presupuesto nuevo o más ajustado, verifica lo siguiente:

  • Compilaciones de lanzamiento: Prueba la compilación que envías, como se describe en Prepara tu entorno de pruebas.
  • Cada estado con presupuesto: Ejecuta tus flujos en primer plano, perceptibles y en segundo plano, y valida el retorno en caliente para cada presupuesto que no sea en primer plano.
  • Dispositivos con poca RAM: Incluye al menos un dispositivo del nivel de RAM más bajo que admitas.
  • Tamaños de ventana: Si usas android:additionalBytesPerDisplayPixel, prueba las ventanas de pantalla dividida y de tamaño ajustable, así como la pantalla completa, ya que el presupuesto ajustado se calcula a partir del tamaño de la ventana de la app.
  • Datos realistas: Realiza pruebas con cuentas grandes, listas largas y muchos elementos multimedia, y con las configuraciones regionales que envías, ya que los conjuntos de trabajo crecen con datos adicionales.
  • Apps de varios procesos: Ejecuta cada proceso que tenga su propia cláusula <memory-budget>.
  • Puertas de regresión: Ejecuta tus pruebas de inicio y de jank de Macrobenchmark existentes con y sin el presupuesto, y falla la prueba si la ejecución presupuestada retrocede más allá de los criterios de aprobación.

Liberación de presupuestos en etapas

Un presupuesto de tiempo de ejecución establecido a través de MemoryBudgetManager solo puede ajustar los límites dentro de los límites de manifiesto de <memory-budget> declarados. No puede aumentar un presupuesto de manifiesto en el entorno de ejecución. No envíes valores de <memory-budget> agresivos en AndroidManifest.xml y planifica relajarlos desde una marca del servidor. En su lugar, lanza los presupuestos nuevos en dos fases:

  1. Valida detrás de la configuración remota: Envía tu APK con un único límite de seguridad <memory-budget> generoso y sin cláusulas perceptible ni background (recomendado) o sin <memory-budget>. Controla los presupuestos de los candidatos detrás de tu experimento del servidor o de la marca de configuración remota, y aplica el presupuesto del estado objetivo en las transiciones del ciclo de vida, como a través de ProcessLifecycleOwner. Captura IllegalArgumentException, que se arroja cuando el presupuesto del entorno de ejecución de un experimento supera un límite de manifiesto estático o de ventana, o el límite de memoria del sistema en un dispositivo con poca RAM. En el bloque catch, llama a clearProcessBudget o clearPackageBudget para que no permanezca activo un presupuesto de tiempo de ejecución más estricto de un estado anterior. Compara los grupos experimentales por nivel de RAM del dispositivo en las métricas que los presupuestos pueden mover: latencia de fotogramas, tiempo de inicio y reanudación en caliente, tiempo de CPU y batería.
  2. Gradúa los presupuestos probados en el manifiesto: Después de que los datos de campo muestren que los presupuestos candidatos cumplen con tus criterios de aprobación en los niveles de RAM del dispositivo, mueve tus presupuestos de estado estable foreground, perceptible y background a las cláusulas <memory-budget>. Lanza esa versión en etapas y observa las mismas métricas.

Durante la validación del tiempo de ejecución, el parámetro de configuración remota es tu interruptor de apagado. Cuando desactivas la marca, los procesos que se inician recientemente omiten la configuración de los presupuestos de tiempo de ejecución, y la llamada a clearProcessBudget o clearPackageBudget borra cualquier presupuesto de tiempo de ejecución activo en un proceso en ejecución. La app vuelve al límite de seguridad de su manifiesto o a no tener presupuesto si no declaraste ninguno, sin actualización de la app. Mantén este parámetro en la configuración de tu propia app y servidor, en lugar de depender del dispositivo o del SO para desactivar los presupuestos.

Trata los presupuestos establecidos solo en el entorno de ejecución como un paso de lanzamiento temporal. Los presupuestos de manifiesto se envían dentro de tu APK firmado y se aplican desde el inicio del proceso sin código de la app. También puedes inspeccionarlos sin ejecutar tu app llamando a PackageManager.getMemoryBudgets, que solo lee declaraciones estáticas de <application>. Para flexibilizar un presupuesto del manifiesto más adelante, se requiere una actualización de la app, pero puedes seguir ejecutando experimentos que ajusten los presupuestos en el tiempo de ejecución por debajo del límite del manifiesto.