Définir des budgets de mémoire pour les applications

Les budgets de mémoire des applications permettent aux applications de déclarer un budget de mémoire pour elles-mêmes, ce qui indique au système de réduire leur utilisation de la mémoire lorsque l'application utilise plus que son budget défini. Cela est particulièrement utile pour les applications système et groupées, ou pour les applications ciblant les appareils à mémoire limitée, où le développeur connaît son ensemble de travail de mémoire attendu et souhaite s'assurer que son application n'utilise pas trop de ressources RAM partagées du système.

Le budget est équilibré grâce à l'éviction et à l'échange de mémoire, qui permettent de supprimer les pages de mémoire qui n'ont pas été utilisées récemment et de concentrer l'empreinte mémoire de l'application sur son ensemble de travail actuel. Lorsqu'une application dépasse son budget déclaré, le système d'exploitation cible la récupération de mémoire spécifiquement sur cette application :

  1. Les pages propres soutenues par des fichiers (comme le code inactif et les composants mappés) sont expulsées en premier, car elles peuvent être relues à partir du stockage si nécessaire.
  2. Les pages modifiées soutenues par des fichiers sont réécrites dans le stockage et supprimées.
  3. Les pages mémoire anonymes (telles que les allocations de tas) sont compressées et transférées vers la zRAM.

Tant que le budget ne dépasse pas l'ensemble de travail, l'application fonctionnera correctement sans utiliser plus de mémoire que le budget défini. Le système d'exploitation évince la mémoire inutilisée et compresse les pages de tas inactives pour les échanger, de sorte que les allocations de mémoire restent limitées sans mettre fin au processus.

Déclarer les budgets dans le fichier manifeste Android

La méthode principale et recommandée pour définir des budgets consiste à déclarer vos budgets de mémoire dans votre AndroidManifest.xml. Il ne nécessite aucun code d'exécution, prend effet immédiatement au démarrage du processus et fournit un contrat clair pour le système d'exploitation.

Déclarer un budget de référence

Pour la plupart des applications, il suffit de définir un seul budget. Déclarez un élément <memory-budget> directement dans la balise <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>

Cela définit un budget de mémoire résidente de 256 Mo pour tous les processus et états du package. Lorsque l'espace mémoire utilisé de l'application dépasse 256 Mo, le système d'exploitation réduit les pages de mémoire inactives à l'aide de l'éviction et de l'échange.

Faire varier les budgets selon l'état du processus

La quantité de mémoire requise par une application varie en fonction de sa visibilité auprès des utilisateurs :

  • Premier plan : le processus héberge une activité visible avec laquelle l'utilisateur interagit. Cet état a généralement la plus grande empreinte en raison de l'UI et des graphiques actifs.
  • Perceptible : le processus est perceptible par l'utilisateur, mais n'héberge pas de fenêtre visible (par exemple, il héberge un service de premier plan de lecture multimédia, une navigation détaillée ou une méthode de saisie active).
  • Arrière-plan : le processus exécute des tâches, des récepteurs ou des synchronisations de données en arrière-plan. Il est censé maintenir une empreinte minimale.

Vous pouvez déclarer plusieurs clauses <memory-budget> pour faire correspondre ces états :

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

Vous n'avez pas besoin de spécifier android:state="foreground" dans la première clause. Une clause sans android:state sert de solution de repli par défaut pour tous les états. Les clauses plus restrictives ci-dessous remplacent le budget lorsque l'application passe à l'état perceptible ou background.

Applications multiprocessus

Si votre application répartit son travail sur plusieurs processus, configurez des budgets de processus dédiés à l'aide de la balise <process> à l'intérieur de <processes>.

Par exemple, prenons l'exemple d'une application de streaming musical (com.example.radio) :

  1. Processus principal : héberge l'UI visible et le moteur de lecture audio (MediaSessionService avec un service de premier plan mediaPlayback). Lorsqu'il est visible, le processus fonctionne avec un budget de premier plan de 180 Mo. Lorsque l'utilisateur quitte l'application alors que la musique continue d'être lue, le processus passe à l'état perceptible, où un budget de 64 Mo est suffisant pour le moteur de lecture et le tampon audio.
  2. Processus de synchronisation (:sync) : processus dédié exécutant la synchronisation des métadonnées en arrière-plan et l'indexation des téléchargements. Comme ce processus n'est actif qu'en arrière-plan, vous n'avez pas besoin de déclarer explicitement state="background". Un seul budget s'applique.
<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'utilisation de la mémoire dans n'importe quel sous-processus est comptabilisée à la fois dans le budget de son processus et dans le budget du package englobant. Un processus subit une pression de mémoire au moment de l'exécution s'il dépasse son budget de processus ou le budget du package, selon le seuil atteint en premier.

Adapter les budgets pour les écrans haute densité

Pour les applications dont l'espace mémoire utilisé évolue de manière significative en fonction du nombre de pixels à dessiner simultanément sur l'écran (comme une application de galerie photo mettant en cache des bitmaps dimensionnés à l'écran), Android fournit deux mécanismes alternatifs pour adapter dynamiquement les budgets aux spécifications de l'écran :

  • Mise à l'échelle par bucket de densité d'affichage (android:additionalMbPerDensity) : ajoute des mégaoctets proportionnellement au ratio de densité de l'écran par rapport à mdpi (1,0x / 160 ppp). Cela convient lorsque l'utilisation de la mémoire évolue en fonction des buckets de densité de l'UI, par exemple pour la mise en cache des éléments de dessin raster ou des éléments d'UI de résolution supérieure :

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

    Sur un écran mdpi (1,0x), le budget est de 180 + 16 × 1 = 196 Mo. Sur un écran xxhdpi (3,0x), le budget est ajusté à 180 + 16 × 3 = 228 Mo.

  • Mise à l'échelle par résolution d'écran physique (android:additionalBytesPerDisplayPixel) : ajoute des octets directement par pixel d'écran physique (largeur × hauteur). C'est idéal pour les applications qui allouent des surfaces graphiques en plein écran, des tampons de rendu ou des caches photo en pleine résolution, où la consommation de mémoire est directement proportionnelle au nombre de pixels bruts de l'écran plutôt qu'aux buckets de densité de l'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" />
    

    Sur un écran 1080p (1080 x 2400 &approx; 2,59 M de pixels), cela ajoute &approx; 41,4 Mo au budget de base. Sur un écran 1440p (1440 x 3120 pixels, soit environ 4,49 millions de pixels), il ajoute environ 71,8 Mo.

Ces deux attributs sont des alternatives. Choisissez l'attribut qui correspond au facteur de scaling principal de votre application et évitez de combiner les deux dans la même clause.

Spécialisation pour les facteurs de forme des appareils

Lorsque vous envoyez un APK sur des téléphones, des tablettes et Wear OS, utilisez l'attribut android:feature pour ajuster les budgets pour différentes cibles matérielles.

Sur les montres Wear OS, la RAM est limitée et l'UI et l'ensemble de fonctionnalités de l'application sont beaucoup plus simples. Vous pouvez déclarer un budget plus serré, spécialisé pour la fonctionnalité 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" />

Règle de résolution : la dernière clause applicable prend effet

Lorsque vous définissez plusieurs éléments <memory-budget> pour une application ou un processus, le système les évalue dans l'ordre dans lequel ils sont déclarés dans le fichier manifeste. La clause budgétaire applicable en dernier est celle qui est appliquée.

Comme le dernier budget applicable est celui qui est pris en compte, l'ordre est important. Placez toujours le budget de référence le plus général en premier, suivi des remplacements plus spécifiques (tels que les clauses spécifiques à un État ou à un matériel).

Documentation de référence sur les attributs XML

Tous les attributs de taille de mémoire sont exprimés en mégaoctets (Mo) et correspondent à la charge memory.current du groupe de contrôle Linux (qui exclut la mémoire partagée comme Zygote).

Attribut Format Par défaut Description
android:maxMb Entier (> 0) Obligatoire Limite de budget de mémoire résidente de référence en Mo.
android:state Enum Tous État du processus auquel ce budget s'applique : foreground, perceptible ou background.
android:additionalMbPerDensity Entier (≥ 0) 0 Nombre de mégaoctets supplémentaires à ajouter par unité de ratio de densité d'affichage par rapport à mdpi (1,0x).
android:additionalBytesPerDisplayPixel Entier (≥ 0) 0 Octets supplémentaires alloués par pixel d'affichage physique (largeur × hauteur), utiles pour les tampons de surface et les bitmaps.
android:feature Chaîne Tous Restreint la clause aux appareils déclarant des fonctionnalités matérielles spécifiques : watch, automotive ou leanback.

API Runtime (option dynamique secondaire)

La solution recommandée pour presque toutes les applications consiste à déclarer les budgets de manière statique dans AndroidManifest.xml. Toutefois, pour les applications avec des charges de travail dynamiques ou pour les expérimentations d'exécution, Android fournit des API de SDK et de NDK d'exécution comme option secondaire.

L'API d'exécution vous permet :

  • Interrogez l'utilisation actuelle de la mémoire et les budgets effectifs.
  • Ajustez dynamiquement le budget de votre processus à la baisse.
  • Écoutez les événements de dépassement de budget pour réduire de manière proactive les caches avant que le système d'exploitation ne déclenche la récupération directe.

API Kotlin (MemoryBudgetManager)

Le service système MemoryBudgetManager est disponible à partir d'Android 17 QPR2 (version 26Q4 du SDK Android, niveau d'API 37.2/Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Récupérer le service

val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)

Utilisation des requêtes et budgets

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

Définir ou effacer des budgets de manière dynamique

Vous pouvez définir un budget plus serré au moment de l'exécution pour limiter la mémoire lors de tâches légères, ou l'effacer une fois la tâche terminée :

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

Écouter les rappels de dépassement de budget

Les applications peuvent enregistrer un écouteur pour être averties lorsque l'utilisation de la mémoire dépasse le seuil du budget. Cela permet à l'application d'effectuer un nettoyage proactif au niveau de l'application (par exemple, en effaçant les caches bitmap en mémoire) avant que le système d'exploitation ne déclenche la latence de récupération directe :

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)

Bonnes pratiques pour les rappels de dépassement de budget :

  • Soyez rapide : les opérations de récupération doivent offrir une aide immédiate. Les calculs complexes effectués sous pression nuisent aux performances.
  • Évitez les allocations : n'allouez pas de nouveaux objets ni ne démarrez de nouveaux threads dans le rappel, car cela peut déclencher une récupération directe immédiate du système d'exploitation.
  • Concentrez-vous sur les cibles à haut rendement : l'éviction de grands bitmaps, de tampons de rendu ou la fermeture de fichiers mappés en mémoire sont beaucoup plus efficaces que la libération de nombreux petits objets.

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

Les applications natives peuvent utiliser l'API C NDK exposée par libandroid.so.

Configuration CMake

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

Inclure l'utilisation des en-têtes et des requêtes

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

Configurer dynamiquement le budget natif

// 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();

Surveiller les événements de sollicitation de la mémoire

Le NDK propose deux façons de surveiller les événements de mémoire :

  1. Observateur de haut niveau (AMemoryBudgetManager_Watcher_create) : surveille les événements sur un ALooper avec un anti-rebond automatique.
  2. Descripteur de fichier de bas niveau : AMemoryBudgetManager_getProcessMemoryPressureFd renvoie un descripteur de fichier natif qui peut être intégré directement dans une boucle de moteur epoll personnalisée.
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);

Exemples d'API Runtime

Les exemples suivants montrent comment implémenter les API d'exécution en Kotlin et en C++.

Exemple Kotlin : éditeur d'images adaptatif

Cet exemple montre une application de retouche photo (com.example.imageeditor) qui augmente dynamiquement son budget mémoire lorsque l'utilisateur ouvre un canevas de retouche multicouche et efface le budget dynamique lorsqu'il revient à la vue de la galerie de miniatures. Il enregistre également un OnOverBudgetListener pour supprimer les bitmaps d'aperçu mis en cache en cas de saturation.

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

Exemple NDK C++ : moteur 3D natif

Cet exemple montre un moteur de jeu C++ natif gérant les budgets de mémoire en fonction du niveau de qualité graphique actif, en utilisant AMemoryBudgetManager_Watcher_create sur un ALooper pour décharger les mipmaps de texture lorsque le budget est dépassé.

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