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. Ciò è 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 working set di memoria previsto e vuole assicurarsi che la sua app non utilizzi troppe risorse RAM condivise del sistema.

Il budget viene mantenuto in equilibrio utilizzando l'eliminazione e lo scambio di memoria per rimuovere le pagine di memoria che non sono state utilizzate di recente, concentrando l'impronta di memoria dell'app sul suo working set attuale. Quando un'app supera il budget dichiarato, i target del sistema operativo vengono recuperati specificamente per quell'app:

  1. Le pagine supportate da file puliti (come codice inattivo e asset mappati) vengono eliminate per prime perché possono essere rilette dallo spazio di archiviazione, se necessario.
  2. Le pagine supportate da file modificate vengono riscritte nello spazio di archiviazione ed eliminate.
  3. Le pagine di memoria anonime (ad esempio le allocazioni dell'heap) vengono compresse e scambiate con zRAM.

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

Dichiarare i budget nel manifest Android

La dichiarazione dei budget di memoria in AndroidManifest.xml è il metodo principale e consigliato per definire i budget. Non richiede codice di runtime, ha effetto immediatamente all'avvio del processo e fornisce un contratto chiaro per il sistema operativo.

Le dichiarazioni <memory-budget> entrano in vigore sui dispositivi con Android 17 QPR2 (livello API 37.2) e versioni successive. Nelle versioni precedenti di Android, il parser del manifest della piattaforma ignora in modo sicuro gli elementi XML non riconosciuti, quindi puoi adottare <memory-budget> senza influire sulla compatibilità con le versioni precedenti.

Dichiarare 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 taglia le pagine di memoria inattive utilizzando l'espulsione e lo scambio.

Variare i budget in base allo stato del processo

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

  • Primo piano: il processo ospita un'attività visibile che interagisce con l'utente. Questo stato in genere ha l'impronta più grande 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 multimediale, un download in background attivo, la navigazione passo passo o un metodo di input attivo).
  • In background: il processo sta eseguendo job, ricevitori o sincronizzazioni dei dati in background. È previsto che mantenga un impatto minimo.

Puoi dichiarare più clausole <memory-budget> per trovare corrispondenze con 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 stato (ad esempio android:state="background"), gli altri stati rimangono senza vincoli di budget dell'app. Se inclusa, una clausola senza android:state funge da fallback predefinito per gli stati non specificati (ad esempio in primo piano), che le clausole successive più restrittive sostituiscono quando l'app passa agli stati perceptible o background.

Corrispondenza tra gli stati del budget e gli stati di elaborazione

La piattaforma esegue il mapping degli stati del budget del manifest agli stati del processo di runtime in base a RunningAppProcessInfo.importance:

  • foreground: interazioni attive e visibili degli utenti, ad esempio l'hosting di un'attività ripresa o il mantenimento dello stato dell'app in primo piano quando lo schermo si spegne.
  • perceptible: carichi di lavoro percepibili dall'utente senza una finestra visibile, ad esempio riproduzione multimediale attiva, navigazione passo passo, acquisizione di fotocamera o microfono, download in background attivi o servizi di sincronizzazione dei dati in primo piano. Sebbene le metriche interne della piattaforma 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 non immediatamente percepibile dall'utente, come processi in background, sveglie, ricevitori di trasmissioni o processi memorizzati nella cache.

La seguente tabella mostra la mappatura dei livelli di importanza di runtime agli stati del manifest:

Manifest android:state Importanza del runtime (RunningAppProcessInfo) Componenti tipici
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Attività visibile ripresa, app in primo piano con schermo bloccato
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Servizi in primo piano di riproduzione, navigazione, download o sincronizzazione di contenuti multimediali attivi
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
IMPORTANCE_CACHED
Processi in background, ricevitori, sincronizzazione in background, processi memorizzati nella cache

Controlla lo stato del processo e il budget della tua app

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

adb shell dumpsys activity processes <package-name>

Il seguente esempio abbreviato mostra il record di processo e le voci di 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 è nello stato di servizio in primo piano. Se la tua app ospita un'attività visibile attiva, viene visualizzata come 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 Process OOM, 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 residente:

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, la fonte del limite (ad esempio AndroidManifest o MemoryBudgetManager) e la memoria residente attuale.

App multiprocesso

Se la tua applicazione divide il lavoro in 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 con un 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 è sempre attivo in background, non devi dichiarare esplicitamente state="background"; si applica 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" />

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

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

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

    </application>
</manifest>

L'utilizzo della memoria in qualsiasi sottoprocesso viene conteggiato sia in base al budget del processo sia a quello del pacchetto contenitore. 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.

Scalare i budget per i display ad alta densità

Per le applicazioni la cui impronta di memoria aumenta in modo significativo in base al numero di pixel da disegnare contemporaneamente sul display, ad esempio un'app galleria fotografica che memorizza nella cache bitmap dimensionati per lo schermo, Android fornisce due meccanismi alternativi per scalare i budget in modo dinamico in base alle specifiche del display:

  • Scala per bucket di densità del display (android:additionalMbPerDensity): aggiunge megabyte in proporzione al rapporto di densità del display rispetto a mdpi (1.0x / 160 dpi). Questa opzione è adatta quando l'utilizzo della memoria viene scalato in base ai bucket di densità dell'interfaccia utente, ad esempio la memorizzazione nella cache di risorse grafiche raster ad alta risoluzione o di asset dell'interfaccia utente:

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

    Su un display mdpi (1x), il budget è 180 + 16 × 1 = 196 MB. Su un display xxhdpi (3x), il budget viene scalato a 180 + 16 × 3 = 228 MB.

  • Scala in base alla risoluzione del display fisico (android:additionalBytesPerDisplayPixel): aggiunge byte direttamente per pixel del display fisico (larghezza × altezza). Questa impostazione è ideale per applicazioni che allocano superfici grafiche a schermo intero, buffer di rendering o cache di foto a risoluzione completa in cui il consumo di memoria è direttamente proporzionale al numero di pixel di visualizzazione non elaborati anziché ai bucket di densità dell'interfaccia utente:

    <!-- 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 ≈ 2,59 milioni di pixel), questo aggiunge circa 41,4 MB al budget di base. Su un display a 1440p (1440 × 3120 e circa 4,49 milioni di pixel), aggiunge circa 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 nella stessa clausola.

Specializzarsi per i fattori di forma dei dispositivi

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

Sugli smartwatch Wear OS, la RAM è limitata e l'interfaccia utente e il set di funzionalità dell'app sono molto più semplici. Puoi dichiarare un budget più ristretto 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: ha effetto 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é vince l'ultimo budget applicabile, l'ordine è importante. Inserisci sempre prima il budget di base più generale, seguito da override più specifici (ad esempio clausole specifiche per stato o hardware).

Riferimento all'attributo XML

Tutti gli attributi di dimensione della memoria sono espressi in megabyte (MB) e mappati alla tariffa cgroup memory.current di 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 base 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 fallback per qualsiasi stato non specificato.
android:additionalMbPerDensity Numero intero (≥ 0) 0 Megabyte aggiuntivi da aggiungere per unità di rapporto di densità di visualizzazione rispetto a mdpi (1x).
android:additionalBytesPerDisplayPixel Numero intero (≥ 0) 0 Byte aggiuntivi allocati per pixel del display fisico (larghezza × altezza), utili per buffer e bitmap di superficie.
android:feature Stringa Qualsiasi Limita la clausola ai dispositivi che dichiarano funzionalità hardware specifiche: watch, automotive o leanback.

API 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 carichi di lavoro dinamici o per sperimentazioni in fase di runtime, Android fornisce API SDK e NDK di runtime come opzione secondaria.

L'API runtime ti consente di:

  • Esegui query sull'utilizzo attuale della memoria e sui budget effettivi.
  • Modifica in modo dinamico il budget del processo riducendolo.
  • Ascolta gli eventi di superamento del budget per tagliare 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 (rilascio SDK secondario, livello API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

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

Impostare o cancellare dinamicamente i budget

Puoi impostare un budget più rigido 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()

Ascolta i richiami di pressione per il superamento del budget

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 cancellare 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 con superamento del budget:

  • Agisci rapidamente: le operazioni di rivendicazione devono offrire un sollievo immediato. I calcoli complessi durante la 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 su target ad alto rendimento: l'eliminazione di bitmap di grandi dimensioni, buffer di rendering o la chiusura di file mappati nella 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
}

Configurare dinamicamente 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();

Monitorare gli eventi di pressione della memoria

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

  1. Osservatore di alto livello (AMemoryBudgetManager_Watcher_create): monitora gli eventi su un ALooper con eliminazione automatica dei rimbalzi.
  2. Descrittore di file di basso livello: AMemoryBudgetManager_getProcessMemoryPressureFd restituisce un descrittore di 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 Runtime

Gli esempi seguenti 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 modifica delle immagini (com.example.imageeditor) il cui manifest dichiara un limite di 256 MB per ospitare 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 dell'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. Registra anche un OnOverBudgetListener per eliminare le bitmap di anteprima memorizzate nella cache sotto 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 NDK C++: motore 3D nativo

Questo esempio mostra un motore grafico C++ nativo che gestisce i budget di memoria in base al livello qualitativo attivo, presupponendo che il manifest dell'applicazione dichiari un limite di 512 MB per ospitare la grafica di alta qualità (android:maxMb="512"). Il motore riduce dinamicamente il budget per i preset di qualità inferiore e utilizza AMemoryBudgetManager_Watcher_create su un ALooper per scaricare le mipmap delle texture quando il budget è 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;
};