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 della 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 in modo specifico 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 elimina la memoria inutilizzata e comprime le pagine heap inattive per lo swap 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.

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, la navigazione passo passo o un metodo di input attivo).
  • Background: il processo esegue 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>

Non è necessario specificare android:state="foreground" nella prima clausola. Una clausola senza android:state funge da fallback predefinito per tutti gli stati. Le clausole più restrittive riportate di seguito sostituiscono il budget quando l'app passa agli stati perceptible o background.

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 in base al budget di 180 MB in primo piano. 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 di 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 in base al bucket di densità del display (android:additionalMbPerDensity): aggiunge megabyte in proporzione al rapporto di densità del display rispetto a mdpi (1x / 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 fattori di forma del dispositivo

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 la clausola di budget più recente.

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 agli attributi 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 di 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.
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 di superficie e bitmap.
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 la sperimentazione 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 ridurre in modo proattivo le cache prima che il sistema operativo attivi il recupero diretto.

API Kotlin (MemoryBudgetManager)

Il servizio di sistema MemoryBudgetManager è disponibile a partire da Android 17 QPR2 (rilascio dell'SDK Android 26Q4, livello API 37.2/ Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Recuperare il servizio

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

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-declared ceiling
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

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

Ascolta i richiami per la pressione sul budget

Le app possono registrare un listener per ricevere una notifica quando la memoria utilizzata supera la soglia del budget. Ciò consente all'app di eseguire una pulizia proattiva a livello di applicazione (ad esempio la cancellazione delle 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 che superano il 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 sui 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.

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

I seguenti esempi mostrano come implementare le API di runtime in Kotlin e C++.

Esempio di Kotlin: editor di immagini adattivo

Questo esempio mostra un'app di modifica delle immagini (com.example.imageeditor) che aumenta dinamicamente il budget di memoria quando l'utente apre un canvas di modifica multilivello e cancella il budget dinamico quando torna alla visualizzazione della galleria di miniature. Registra anche un OnOverBudgetListener per eliminare le bitmap dell'anteprima memorizzate nella cache in caso di necessità.

package com.example.imageeditor.ui

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

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager

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

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

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

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

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

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

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

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

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, utilizzando 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;
};