Impostare i budget di memoria delle app

I budget di memoria delle app consentono alle app di dichiarare un budget di memoria per se stesse, il che indica al sistema di ridurre l'utilizzo della memoria quando l'app utilizza più del budget impostato. Questo è particolarmente utile per le app di sistema e in bundle o per le app destinate a dispositivi con memoria limitata, in cui lo sviluppatore conosce il set di lavoro della memoria previsto (la memoria di cui l'app ha bisogno attivamente per ciò che l'utente sta facendo al momento) e vuole assicurarsi che l'app non utilizzi troppe risorse di RAM condivise del sistema.

Il budget viene mantenuto bilanciato utilizzando l'eliminazione e lo scambio della memoria per rimuovere le pagine di memoria che non sono state utilizzate di recente, concentrando il footprint della memoria dell'app sul suo set di lavoro attuale. Quando un'app supera il budget dichiarato, il sistema operativo indirizza il recupero specificamente a quell'app:

  1. Le pagine pulite con backup su file (come il codice inattivo e gli asset mappati) vengono rimosse per prime perché possono essere rilette dallo spazio di archiviazione, se necessario.
  2. Le pagine dirty basate su file vengono riscritte nello spazio di archiviazione ed espulse.
  3. Le pagine di memoria anonime (come le allocazioni heap) vengono compresse e scambiate con zRAM.

Se il working set non supera il budget, l'app funzionerà correttamente senza utilizzare più memoria di quella prevista dal budget impostato. Il sistema operativo rimuove la memoria inutilizzata e comprime le pagine heap inattive per lo scambio, in modo che le allocazioni della memoria rimangano limitate senza terminare il processo.

Cosa succede quando un'app supera il budget

Il superamento di un budget di memoria non causa l'arresto o l'arresto anomalo dell'app da parte del sistema. Quando un'app utilizza più memoria di quella consentita dal budget, il sistema operativo trova la memoria che l'app non utilizza da un po' di tempo e la mette da parte in modo sicuro finché non è nuovamente necessaria. In questo modo, viene liberato spazio per le nuove allocazioni della memoria dell'app e l'utilizzo complessivo della memoria dell'app rimane entro il budget.

Finché esiste un margine tra ciò di cui l'app ha bisogno in un determinato momento (il suo working set) e il budget impostato, l'app continua a funzionare normalmente. Se un'app imposta un budget inferiore al suo working set, le prestazioni dell'app in fase di runtime possono rallentare.

Per scoprire come il sistema operativo monitora e recupera la memoria, consulta la guida all'architettura della memoria:

Dichiara i budget nel file manifest di Android

Dichiarare i budget di memoria nel file AndroidManifest.xml è il metodo principale e consigliato per definire i budget. Non richiede codice di runtime, ha effetto immediato all'avvio del processo e fornisce un contratto chiaro per il sistema operativo.

Le dichiarazioni <memory-budget> hanno effetto sui dispositivi con Android 17 QPR2 (livello API 37.2) e versioni successive. Nelle versioni precedenti di Android, l'analizzatore sintattico del file manifest della piattaforma ignora in modo sicuro gli elementi XML non riconosciuti, in modo da poter adottare <memory-budget> senza influire sulla compatibilità con le versioni precedenti.

Dichiara un budget di riferimento

Per la maggior parte delle app, è sufficiente definire un unico budget per l'applicazione. Dichiara un elemento <memory-budget> direttamente all'interno del tag <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>

In questo modo viene impostato un budget di memoria residente di 256 MB per tutti i processi e gli stati del pacchetto. Quando il footprint della memoria dell'app supera i 256 MB, il sistema operativo riduce le pagine di memoria non attive utilizzando l'eliminazione e lo scambio.

Varia i budget in base allo stato del processo

Un'app richiede quantità di memoria diverse a seconda della sua visibilità per l'utente:

  • In primo piano: il processo ospita un'attività visibile che interagisce con l'utente. In genere, questo stato ha l'impatto maggiore a causa dell'interfaccia utente e della grafica attive.
  • Percepibile: il processo è percepibile dall'utente, ma non ospita una finestra visibile (ad esempio, ospita un servizio in primo piano di riproduzione di contenuti multimediali, un download in background attivo, la navigazione passo passo o un metodo di immissione attivo).
  • In background: il processo sta eseguendo job, ricevitori o sincronizzazioni di dati in background. Si prevede che mantenga un impatto minimo.

Puoi dichiarare più clausole <memory-budget> in modo che corrispondano a questi stati:

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

Una clausola di base senza android:state non è obbligatoria; se specifichi solo clausole specifiche per lo stato (ad esempio android:state="background"), gli altri stati rimangono non vincolati da un budget dell'app. Se inclusa, una clausola senza android:state funge da riserva predefinita per gli stati non specificati (ad esempio foreground), che viene sostituita dalle clausole successive più restrittive quando l'app passa agli stati perceptible o background.

Quando dimensioni i budget per ogni stato, scegli come target il set di lavoro attivo di quello stato anziché la memoria residente di picco (limite massimo RSS):

  • Dimensiona i budget foreground per un rendering fluido dei frame: assegna alla UI in primo piano un margine sufficiente per il suo working set attivo, in modo che il recupero sincrono non ritardi il rendering dei frame. Se la tua app include un flusso di lavoro autonomo che richiede molta memoria (ad esempio un editor di immagini o video ad alta risoluzione), valuta la possibilità di spostare l'attività in un processo dedicato (consulta App multiprocesso) in modo che le relative allocazioni vengano preventivate in modo indipendente e rimosse quando l'utente esce.
  • Dimensiona background i budget in modo rigoroso per il lavoro headless: un RSS in background elevato nella telemetria sul campo spesso riflette le allocazioni della UI conservate dopo l'uscita dal primo piano (consulta Collocazione di un servizio associato con una UI ad alta intensità di memoria e Rilascia la memoria in risposta agli eventi) o letture di file una tantum durante l'indicizzazione e le sincronizzazioni in background. Poiché il lavoro in background headless non ha scadenze per i frame, l'eliminazione delle pagine della cache dei file una tantum e lo scambio delle pagine heap inattive con zRAM quando una sincronizzazione in background alloca brevemente oltre il suo budget non causa jank visibili all'utente, impedendo al contempo che i picchi in background eliminino l'app in primo piano dell'utente.

Come gli stati del budget vengono mappati agli stati del processo

La piattaforma mappa gli stati del budget del manifest agli stati dei processi di runtime in base a RunningAppProcessInfo.importance, che Activity Manager calcola sia dai componenti attivi del processo sia da eventuali associazioni di servizi o query del provider di contenuti in entrata da altre app:

  • foreground: interazioni utente attive e visibili, ad esempio l'hosting di un'attività ripresa, il mantenimento dello stato dell'app principale quando lo schermo si spegne o l'hosting di un servizio o di un provider di contenuti attivamente associato o interrogato dall'app in primo piano principale.
  • perceptible: workload percepibili dall'utente senza una finestra visibile, come la riproduzione di contenuti multimediali attivi, la navigazione passo passo, l'acquisizione della fotocamera o del microfono, i download in background attivi, i servizi in primo piano di sincronizzazione dei dati o i servizi associati da un client in primo piano utilizzando flag come BIND_NOT_FOREGROUND (consulta Controllo dell'ereditarietà con i flag BIND). Sebbene le metriche della piattaforma interna possano etichettare alcuni di questi workload come PROCESS_STATE_IMPORTANT_FOREGROUND, questa costante interna non indica una finestra visibile ed è regolata dal budget perceptible.
  • background: lavoro che non è immediatamente percepibile dall'utente, come job in background, sveglie, broadcast receiver o l'attività precedente dopo che l'utente si è allontanato.

La seguente tabella mostra come i livelli di importanza di runtime vengono mappati agli stati del manifest:

Manifest android:state Importanza runtime (RunningAppProcessInfo) Componenti tipici
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Attività visibile ripresa, app in primo piano con schermo bloccato, servizio o provider di contenuti associato all'app in primo piano
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Servizi in primo piano di riproduzione di contenuti multimediali, navigazione, download o sincronizzazione attivi; servizio associato a flag di servizio in primo piano o visibili
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
Job in background, broadcast receiver, sincronizzazione in background, attività precedente o app Home in background

Per maggiori dettagli su come le associazioni dei client elevano un processo host di un servizio o di un provider e su come il processo torna a background o cached dopo che il client si scollega o si sposta fuori dallo schermo, consulta Associazione dei servizi e stati dei processi.

Quando un processo entra in uno stato memorizzato nella cache (IMPORTANCE_CACHED) senza componenti attivi, la piattaforma rilascia il budget di memoria attiva e gestisce la memoria tramite il recupero standard dei processi memorizzati nella cache.

Ispeziona lo stato del processo e il budget dell'app

Un modo per ispezionare lo stato e l'importanza del processo attivo della tua app durante lo sviluppo è eseguire una query su Activity Manager utilizzando ADB:

adb shell dumpsys activity processes <package-name>

Il seguente esempio abbreviato mostra le voci del record di processo e del controllo OOM per un'app che esegue un servizio in primo piano:

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

In questo output:

  • curProcState=4 e state: cur=FGS indicano che il processo si trova nello stato del servizio in primo piano. Se la tua app ospita un'attività visibile attiva, viene visualizzato TOP. Se è in esecuzione un componente in primo piano importante (ad esempio un download o una sincronizzazione), viene visualizzato come IMPF.
  • Nella tabella di controllo OOM del processo, prcp indica che il processo viene valutato in base al livello di priorità percepibile, che corrisponde al budget perceptible.

Per esaminare il budget di memoria attualmente applicato e l'utilizzo della memoria utilizzata:

adb shell dumpsys meminfo <package-name>

A partire da Android 17 QPR2, questo output include una sezione Budget di memoria che mostra il limite massimo attivo, l'origine del limite (ad esempio AndroidManifest o MemoryBudgetManager) e la memoria residente corrente.

App multiprocesso

Se la tua applicazione suddivide il lavoro tra più processi, configura budget di processo dedicati utilizzando il tag <process> all'interno di <processes>.

Ad esempio, considera un'app di musica in streaming (com.example.radio):

  1. Processo principale: ospita l'interfaccia utente visibile e il motore di riproduzione audio (MediaSessionService con un servizio in primo piano mediaPlayback). Quando è visibile, il processo opera nel budget in primo piano di 180 MB. Quando l'utente esce dall'app mentre la musica continua a essere riprodotta, il processo entra nello stato perceptible, in cui un budget di 64 MB è sufficiente per il motore di riproduzione e il buffer audio.
  2. Processo di sincronizzazione (:sync): processo dedicato che esegue la sincronizzazione dei metadati in background e l'indicizzazione dei download. Poiché questo processo è attivo solo in background, non è necessario dichiarare esplicitamente state="background"; viene applicato un unico budget.
<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>

Quando dichiari un blocco <processes> nel manifest, elenca le voci <process> solo per i processi che dichiarano un <memory-budget>. Se vuoi impostare un budget solo per sottoprocessi specifici, lasciando il resto dell'app al limite di sistema per il dispositivo, puoi omettere gli elementi <memory-budget> direttamente sotto <application>.

L'utilizzo della memoria in qualsiasi processo secondario viene conteggiato sia nel budget del processo sia nel budget del pacchetto che lo contiene. Un processo subisce una pressione della memoria in fase di runtime se supera il budget del processo o il budget del pacchetto, a seconda della soglia raggiunta per prima.

Scala i budget per i display ad alta densità

Per le applicazioni il cui footprint della memoria varia in modo significativo in base al numero di pixel da disegnare contemporaneamente sul display, ad esempio un'app di galleria fotografica che memorizza nella cache bitmap di dimensioni adatte allo schermo, Android fornisce due meccanismi alternativi per scalare i budget in modo dinamico con le specifiche del display:

  • Scala in base al bucket di densità del display (android:additionalMbPerDensity): aggiunge megabyte in modo proporzionale al quadrato del rapporto di densità del display rispetto a mdpi (1,0x / 160 dpi), perché gli elementi drawable raster 2D e gli asset dell'interfaccia utente vengono scalati sia in larghezza che in altezza:

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

    Su un display mdpi (1,0x), il budget è 180 + 16 × 12 = 196 MB. Su un display xxhdpi (3,0x), il budget aumenta a 180 + 16 × 32 = 324 MB.

  • Scala in base alla risoluzione fisica del display (android:additionalBytesPerDisplayPixel): aggiunge byte direttamente per pixel del display fisico (larghezza × altezza). Questo è l'ideale per le applicazioni che allocano superfici grafiche a schermo intero, buffer di rendering o cache di foto a risoluzione piena in cui il consumo di memoria scala direttamente con il numero di pixel del display non elaborati anziché con i bucket di densità della UI:

    <!-- 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" />
    

    Su un display 1080p (1080 × 2400 &approx; 2,59 milioni di pixel), questo aggiunge &approx; 41,4 MB al budget di base. Su un display da 1440p (1440 × 3120 &approx; 4,49 milioni di pixel), aggiunge &approx; 71,8 MB.

Questi due attributi sono alternativi. Scegli l'attributo che corrisponde al fattore di scalabilità principale della tua app ed evita di combinarli entrambi nella stessa clausola.

Specializzati per i fattori di forma dei dispositivi

Quando distribuisci un APK su smartphone, tablet e Wear OS, utilizza l'attributo android:feature per regolare i budget per i diversi target hardware.

Sugli smartwatch Wear OS, la RAM è limitata e l'UI e l'insieme di funzionalità dell'app sono molto più semplici. Puoi dichiarare un budget più limitato specializzato per la funzionalità 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" />

Regola di risoluzione: viene applicata l'ultima clausola applicabile

Quando definisci più elementi <memory-budget> per un'applicazione o un processo, il sistema li valuta nell'ordine in cui sono dichiarati nel manifest. Viene applicata l'ultima clausola di budget applicabile.

Poiché l'ultimo budget applicabile ha la precedenza, l'ordine è importante. Inserisci sempre prima il budget di riferimento più generale, seguito da override più specifici (ad esempio clausole specifiche per lo stato o l'hardware).

Riferimento degli attributi XML

Tutti gli attributi relativi alle dimensioni della memoria sono espressi in megabyte (MB) e mappati all'addebito memory.current del cgroup Linux (che esclude la memoria condivisa come Zygote).

Attributo Formato Predefinito Descrizione
android:maxMb Numero intero (> 0) Obbligatorio Il limite del budget di memoria residente di riferimento in MB.
android:state Enum Qualsiasi Lo stato del processo a cui si applica questo budget: foreground, perceptible o background. Se omessa, la clausola funge da riserva per qualsiasi stato non specificato.
android:additionalMbPerDensity Numero intero (≥ 0) 0 Megabyte aggiuntivi moltiplicati per il quadrato del rapporto di densità del display rispetto a mdpi (1,0x).
android:additionalBytesPerDisplayPixel Numero intero (≥ 0) 0 Byte aggiuntivi allocati per pixel del display fisico (larghezza × altezza), utili per i buffer di superficie e le bitmap.
android:feature Stringa Qualsiasi Limita la clausola ai dispositivi che dichiarano funzionalità hardware specifiche: watch, automotive o leanback.

API di runtime (opzione dinamica secondaria)

La dichiarazione statica dei budget in AndroidManifest.xml è la soluzione preferita per quasi tutte le app. Tuttavia, per le applicazioni con workload dinamici o per la sperimentazione in fase di runtime, Android fornisce le API SDK e NDK di runtime come opzione secondaria.

L'API di runtime ti consente di:

  • Esegui query sull'utilizzo attuale della memoria e sui budget effettivi.
  • Modificare in modo dinamico il budget del processo verso il basso.
  • Ascolta gli eventi di superamento del budget per ridurre in modo proattivo le cache prima che il sistema operativo attivi il recupero diretto.

API SDK Android (MemoryBudgetManager)

Il servizio di sistema MemoryBudgetManager è disponibile per le app scritte in Kotlin e Java a partire da Android 17 QPR2 (release SDK secondaria, livello API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Recupera il servizio

Prima di accedere a MemoryBudgetManager, verifica che sul dispositivo non sia in esecuzione una versione precedente ad Android 17 QPR2 utilizzando SDK_INT_FULL:

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

Utilizzo delle query e budget

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

Imposta o cancella i budget in modo dinamico

Puoi impostare un budget più restrittivo in fase di runtime per limitare la memoria durante le attività leggere o cancellarlo al termine dell'attività:

// 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 differenza delle clausole del manifest <memory-budget>, che cambiano automaticamente i budget ogni volta che il processo passa tra gli stati foreground, perceptible e background, un budget dinamico impostato tramite MemoryBudgetManager applica un unico limite massimo finché il codice non lo aggiorna o lo cancella. Se utilizzi l'API di runtime per eseguire esperimenti sul campo o gestire limiti specifici per lo stato, aggiorna o cancella il budget dinamico durante le transizioni del ciclo di vita (ad esempio, utilizzando ProcessLifecycleOwner o i callback del ciclo di vita dell'attività, come mostrato nell'esempio di editor di immagini adattivo).

Prestazioni e idempotenza

Quando utilizzi MemoryBudgetManager in fase di runtime, tieni presente le seguenti caratteristiche:

  • Idempotenza: tutte le API di mutazione del budget sono idempotenti. La chiamata ripetuta di clearProcessBudget() o clearPackageBudget() o quando non è attivo alcun budget dinamico è un'operazione sicura. Analogamente, l'impostazione consecutiva di setProcessBudgetBytes() o setPackageBudgetBytes() sullo stesso valore non attiva riconfigurazioni di sistema ridondanti.
  • Frequenza delle chiamate: l'impostazione, l'aggiornamento o la cancellazione di un budget effettua una chiamata tra processi al servizio di sistema per aggiornare il controller della memoria del sistema operativo per il processo. Sebbene sia leggero, non è completamente senza costi. Chiama queste API durante le transizioni discrete del ciclo di vita e i milestone delle attività (ad esempio l'accesso o l'uscita da un'attività, l'avvio o il completamento dell'elaborazione in background o la gestione delle richieste di sintesi vocale) ed evita di chiamarle in loop stretti, thread di elaborazione audio o routine di rendering per frame.
  • Frequenza delle query: le proprietà delle query processCurrentUsageBytes e packageCurrentUsageBytes recuperano la memoria utilizzata in tempo reale dal servizio di sistema. Sebbene siano veloci, le query devono essere richiamate on demand (ad esempio durante le transizioni delle attività o all'interno dei callback OnOverBudgetListener) anziché essere sottoposte a polling in loop continui.

Ascolta i callback di pressione per budget in eccesso

Le app possono registrare un listener per ricevere una notifica quando la memoria utilizzata supera la soglia del budget. In questo modo, l'app può eseguire una pulizia proattiva a livello di applicazione (ad esempio, svuotando le cache bitmap in memoria) prima che il sistema operativo attivi la latenza di recupero diretto:

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)

Best practice per i callback di superamento del budget:

  • Agisci rapidamente: le operazioni di recupero devono offrire un sollievo immediato. I calcoli complessi durante i periodi di pressione peggiorano le prestazioni.
  • Evita le allocazioni: non allocare nuovi oggetti o avviare nuovi thread all'interno del callback, in quanto ciò può attivare il recupero diretto immediato del sistema operativo.
  • Concentrati sui target ad alto rendimento: l'eliminazione di bitmap di grandi dimensioni, buffer di rendering o la chiusura di file mappati in memoria è molto più efficace del rilascio di molti piccoli oggetti.

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

Le app native possono utilizzare l'API C NDK esposta da libandroid.so a partire da Android 17 QPR2 (livello API 37.2).

Configurazione CMake

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

Includi l'utilizzo di intestazioni e query

#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 in modo dinamico il budget 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();

Monitora gli eventi di pressione della memoria

L'NDK offre due modi per monitorare gli eventi di memoria:

  1. Watcher di alto livello (AMemoryBudgetManager_Watcher_create): monitora gli eventi su un ALooper con debouncing automatico.
  2. Descrittore del file di basso livello: AMemoryBudgetManager_getProcessMemoryPressureFd restituisce un descrittore del file nativo che può essere integrato direttamente in un ciclo del motore epoll personalizzato.
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);

Esempi di API di runtime

I seguenti esempi mostrano come implementare le API di runtime utilizzando l'API SDK Android (scritta in Kotlin) e l'API NDK nativa (scritta in C++).

Esempio di SDK Android: editor di immagini adattive

Questo esempio mostra un'app di editing di immagini (com.example.imageeditor) il cui manifest dichiara un limite di 256 MB per supportare il canvas multilivello:

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

Quando l'utente sfoglia la galleria di miniature leggera, l'app utilizza l'API SDK Android in Kotlin per ridurre dinamicamente il budget del processo a 96 MB. Quando l'utente apre il canvas di modifica multilivello, l'app cancella il budget dinamico per ripristinare il limite massimo del manifest di 256 MB. Inoltre, registra un OnOverBudgetListener per rimuovere le bitmap di anteprima memorizzate nella cache in caso di pressione.

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)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

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

    /**
     * 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"
    }
}

Esempio di NDK C++: motore 3D nativo

Questo esempio mostra un motore grafico C++ nativo che gestisce i budget di memoria in base al livello di qualità grafica attivo, supponendo che il manifest dell'applicazione dichiari un limite di 512 MB per supportare la grafica di alta qualità (android:maxMb="512"). Il motore grafico riduce dinamicamente il budget per i preset di qualità inferiore e utilizza AMemoryBudgetManager_Watcher_create su un ALooper per scaricare i mipmap delle texture quando il budget viene superato.

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