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 recorte su uso de memoria cuando la app use más de su presupuesto establecido. Esto es especialmente útil para las apps del sistema y empaquetadas, o para las apps orientadas a dispositivos con memoria limitada, en las que el desarrollador conoce el conjunto de trabajo de memoria esperado (la memoria que la app necesita de forma activa para lo que el usuario está haciendo en ese momento) 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 el espacio en memoria de la app en su conjunto de trabajo actual. Cuando una app supera su presupuesto declarado, el sistema operativo se orienta específicamente a esa app para recuperar recursos:
- 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.
- Las páginas respaldadas por archivos modificadas se vuelven a escribir en el almacenamiento y se expulsan.
- Las páginas de memoria anónimas (como las asignaciones de montón) se comprimen y se intercambian a zRAM.
Siempre que el conjunto de trabajo no supere el presupuesto, la app funcionará bien sin usar más memoria de la que tiene asignada. El sistema operativo expulsa la memoria sin usar y comprime las páginas de montón inactivas para intercambiarlas, de modo que las asignaciones de memoria permanezcan limitadas sin finalizar el proceso.
Qué sucede cuando una app excede su presupuesto
Exceder un presupuesto de memoria no hace que el sistema finalice o falle tu app. En cambio, cuando una app usa más memoria de la que permite su presupuesto, el SO encuentra memoria que la app no usó en algún momento y la guarda de forma segura hasta que se vuelva a necesitar. Esto deja espacio para nuevas asignaciones de memoria de la app y mantiene el uso general de la memoria de la app dentro del 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:
- Recuperación y cambio de memoria: Explica cómo el SO carga páginas privadas de montón anónimas y respaldadas por archivos en tu app, y recupera páginas inactivas cuando se supera el presupuesto.
- Conceptos fundamentales: Explica por qué la memoria de Zygote compartida se excluye del presupuesto de tu app y cómo se relacionan los presupuestos residentes con el RSS anónimo + intercambio en la telemetría de campo.
- Mapas de bits y memoria: Explica cómo se contabilizan los mapas de bits de hardware respaldados por GPU y las asignaciones de DMA-BUF fuera del montón y del presupuesto de memoria de tu app.
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 entorno de ejecución, se aplica de inmediato cuando se inicia el proceso y proporciona un contrato claro para el sistema operativo.
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 de forma segura los elementos XML no reconocidos, por lo que puedes adoptar <memory-budget> sin afectar la retrocompatibilidad.
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 base sin android:state. Si solo especificas
cláusulas específicas del estado (por ejemplo, android:state="background"), otros estados
permanecen sin restricciones por un presupuesto de la app. Cuando se incluye, una cláusula sin android:state actúa como el resguardo predeterminado para los estados no especificados (como el primer plano), que las cláusulas más restrictivas posteriores anulan cuando la app pasa a los estados perceptible o background.
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 de
foregrounda los presupuestos 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
backgroundpara 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.
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 del entorno 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 de contenido multimedia activo, 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 comoBIND_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 comoPROCESS_STATE_IMPORTANT_FOREGROUND, esa constante interna no significa una ventana visible y se rige por el presupuesto deperceptible.background: Es el trabajo que el usuario no percibe de inmediato, como los trabajos en segundo plano, las alarmas, los receptores de transmisiones o la actividad anterior después de que el usuario sale de ella.
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_FOREGROUNDIMPORTANCE_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_SERVICEIMPORTANCE_VISIBLE |
Servicios en primer plano de reproducción de contenido multimedia, navegación, descargas o sincronización activos; servicio vinculado con marcas de servicio en primer plano o visibles |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
Trabajos en segundo plano, receptores, sincronización en segundo plano, actividad anterior o app de Home en segundo plano |
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 libera su presupuesto de memoria activa y administra su memoria a través de la recuperación estándar de procesos almacenados en caché.
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=4ystate: cur=FGSreflejan que el proceso se encuentra en el estado de servicio en primer plano. Si tu app aloja una actividad visible activa, aparecerá comoTOP. Si ejecuta un componente importante en primer plano (como una descarga o sincronización), aparece comoIMPF.- En la tabla de control de OOM de procesos,
prcpindica que el proceso se evalúa en el nivel de prioridad perceptible, que corresponde al presupuesto deperceptible.
Para inspeccionar el presupuesto de memoria que se aplica actualmente y el uso de memoria residente, haz lo siguiente:
adb shell dumpsys meminfo <package-name>
A partir de Android 17 QPR2, este resultado incluye una sección Memory Budget que muestra el límite máximo activo, la fuente del límite (como AndroidManifest o MemoryBudgetManager) y la memoria residente actual.
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, imagina una app de transmisión de música (com.example.radio):
- Proceso principal: Aloja la IU visible y el motor de reproducción de audio (
MediaSessionServicecon un servicio en primer plano demediaPlayback). 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 ingresa al estadoperceptible, en el que un presupuesto de 64 MB es suficiente para el motor de reproducción y el búfer de audio. - 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. Debido a que este proceso solo está activo en segundo plano, no es necesario que declares de forma explícitastate="background"; 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 pantallas de alta densidad
En el caso de las aplicaciones cuyo espacio en memoria escala de forma significativa 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 con el tamaño de la pantalla), 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 pantalla (
android:additionalMbPerDensity): Agrega megabytes de manera proporcional al cuadrado de la proporción de densidad de la pantalla en relación conmdpi(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 el alto:<!-- 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 pantallaxxhdpi(3.0x), el presupuesto se escala a 180 + 16 × 32 = 324 MB.Escala por 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 plataformas de gráficos 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 escala directamente con el recuento de píxeles de la pantalla sin procesar, en lugar de los buckets 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 millones de píxeles), esto agrega ≈ 41.4 MB al presupuesto de referencia. En una pantalla de 1440p (1440 × 3120 ≈ 4.49 millones de píxeles), agrega ≈ 71.8 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.
Especialízate en 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 simples. 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 aplicación o un proceso, el sistema los evalúa en el orden en que se declaran en el manifiesto. Se aplica la última cláusula de presupuesto aplicable.
Debido a que se aplica el último presupuesto aplicable, el orden es importante. Siempre coloca primero el presupuesto de referencia más general, seguido de anulaciones más específicas (como 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 de memory.current de 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 | El estado del proceso al que se aplica este presupuesto: foreground, perceptible o background. Si se omite, la cláusula actúa como un resguardo para cualquier estado no especificado. |
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 pantalla física (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 (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 entorno de ejecución, Android proporciona APIs del SDK y del NDK del entorno 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 vigentes
- Ajusta de forma dinámica el presupuesto de tu proceso a la baja.
- Escucha los eventos de exceso de presupuesto para recortar las cachés de forma proactiva antes de que el sistema operativo active la recuperación directa.
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 effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
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 livianas 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 ceiling 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 de manifiesto <memory-budget>, que cambian automáticamente los presupuestos
cada vez que el proceso pasa entre los estados foreground, perceptible y
background, un presupuesto dinámico establecido a través de MemoryBudgetManager aplica
un único límite hasta que tu 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 (por ejemplo, con 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()oclearPackageBudget()de forma repetida o cuando no hay un presupuesto dinámico activo es una operación nula segura. Del mismo modo, establecersetProcessBudgetBytes()osetPackageBudgetBytes()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 entrar o salir de una actividad, iniciar o completar el procesamiento 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
processCurrentUsageBytesypackageCurrentUsageBytesrecuperan el uso de memoria en vivo del servicio del sistema. Si bien son rápidas, las consultas se deben invocar a pedido (como durante las transiciones de tareas o dentro de las devoluciones de llamada deOnOverBudgetListener) en lugar de sondearse en bucles continuos.
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
Looperen segundo plano: Pasa unLooperen segundo planoHandlerThreadaregisterProcessOverBudgetListeneroregisterPackageOverBudgetListenersiempre 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. ReservaLooper.getMainLooperpara 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 recupera primero las páginas inactivas y, si la presión continúa, el sistema sigue recuperando 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(ounregisterPackageOverBudgetListener) dentro de la devolución de llamada o protege la limpieza con una marca de tiempo de espera oAtomicBoolean. - Regístrate antes de ajustar un presupuesto: Si el uso de memoria actual ya
supera un presupuesto nuevo cuando llamas a
setProcessBudgetBytesosetPackageBudgetBytes, 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. - Sé rápido: 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.
API de NDK nativa (<android/memory_budget_manager.h>)
Las aplicaciones nativas pueden usar la API de NDK de C que expone libandroid.so a partir de Android 17 QPR2 (nivel de API 37.2).
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 de forma dinámica el presupuesto nativo
// 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 (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();
Supervisa los eventos de presión de memoria
El NDK proporciona dos formas de supervisar los eventos de memoria:
- Observador de alto nivel (
AMemoryBudgetManager_Watcher_create): Supervisa los eventos en unALoopercon eliminación automática de rebotes. - Descriptor de archivo de bajo nivel:
AMemoryBudgetManager_getProcessMemoryPressureFddevuelve un descriptor de archivo nativo que se puede integrar directamente en un bucle de motorepollpersonalizado.
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 a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
background_looper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000 /* debounce_ms */,
&onMemoryPressure,
NULL /* userdata */
);
// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);
Vincula el observador al ALooper de un subproceso de trabajador en segundo plano, en lugar de tu subproceso de IU o renderización principal, de modo que la expulsión de la caché no compita con la renderización de fotogramas. Crea el observador antes de ajustar un presupuesto con
AMemoryBudgetManager_setProcessBudget o
AMemoryBudgetManager_setPackageBudget para que tu app no se pierda el evento inicial
de exceso de presupuesto.
Mientras el proceso o el paquete sigan superando el presupuesto y el sistema operativo continúe reclamando memoria, el observador invocará la devolución de llamada de forma repetida (como máximo, una vez por intervalo de debounce_ms). Haz que tu devolución de llamada sea idempotente o llama a AMemoryBudgetManager_Watcher_destroy (que es seguro llamar desde la devolución de llamada) si tu app solo necesita una notificación única.
Ejemplos de la API de Runtime
En los siguientes ejemplos, se muestra cómo implementar las APIs de tiempo de ejecución con la API del SDK de Android (escrita en Kotlin) y la API de NDK nativo (escrita en C++).
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 Looper HandlerThread 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)
}
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"
}
}
Ejemplo de NDK en C++: Motor 3D nativo
En este ejemplo, se muestra un motor de juego nativo de C++ que administra los presupuestos de memoria según el nivel de calidad de gráficos activo, suponiendo que el manifiesto de la aplicación declara un límite de 512 MB para admitir gráficos de alta calidad (android:maxMb="512"). El motor inicia AMemoryBudgetManager_Watcher_create en el ALooper de un subproceso de trabajo en segundo plano antes de ajustar el presupuesto para los ajustes predeterminados de menor calidad y descarga los mapas MIP de textura 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;
}
// Call on a background worker ALooper before tightening the budget with setQualityBudget
bool startMonitoring(ALooper* backgroundLooper) {
if (!backgroundLooper) return false;
// Monitor process budget events, debounced to at most once every 1000ms
mWatcher = AMemoryBudgetManager_Watcher_create(
backgroundLooper,
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, idempotent, 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;
};