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 l'ensemble de travail de mémoire attendu (la mémoire dont l'application a activement besoin pour ce que l'utilisateur fait actuellement) et souhaite s'assurer que son application n'utilise pas trop de ressources de RAM partagées du système.

Le budget est équilibré à l'aide de l'éviction et de l'échange de mémoire pour supprimer les pages de mémoire qui n'ont pas été utilisées récemment, en concentrant 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 spécifiquement cette application pour récupérer de la mémoire :

  1. Les pages propres sauvegardées dans un fichier (comme le code inactif et les éléments mappés) sont supprimées en premier, car elles peuvent être lues à nouveau depuis l'espace de stockage si nécessaire.
  2. Les pages modifiées sauvegardées dans un fichier sont réécrites dans l'espace de stockage et supprimées.
  3. Les pages de mémoire anonymes (telles que les allocations de segments de mémoire) sont compressées et transférées vers la zRAM.

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

Que se passe-t-il lorsqu'une application dépasse son budget ?

Le dépassement d'un budget de mémoire n'entraîne pas l'arrêt ni le plantage de votre application par le système. En revanche, lorsqu'une application utilise plus de mémoire que son budget ne le permet, l'OS trouve de la mémoire que l'application n'a pas utilisée depuis un certain temps et la met de côté en toute sécurité jusqu'à ce qu'elle soit de nouveau nécessaire. Cela permet de libérer de l'espace pour de nouvelles allocations de mémoire et de maintenir l'utilisation globale de la mémoire de l'application dans les limites du budget.

Tant qu'il existe une marge entre les besoins de l'application à un moment donné (son ensemble de travail) et le budget défini, l'application continue de s'exécuter normalement. Si une application définit un budget inférieur à son ensemble de travail, ses performances au moment de l'exécution peuvent être ralenties.

Pour savoir comment l'OS suit et récupère la mémoire, consultez le guide sur l'architecture de la mémoire :

Déclarer des budgets dans le fichier manifeste Android

La déclaration de vos budgets de mémoire dans votre AndroidManifest.xml est la méthode principale et recommandée pour définir des budgets. 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.

Les déclarations <memory-budget> prennent effet sur les appareils équipés d'Android 17 QPR2 (niveau d'API 37.2) ou version ultérieure. Sur les versions d'Android antérieures, l'analyseur de fichiers manifestes de la plate-forme ignore de manière sécurisée les éléments XML non reconnus. Vous pouvez donc adopter <memory-budget> sans affecter la rétrocompatibilité.

Déclarer un budget de référence

Pour la plupart des applications, il suffit de définir un seul budget pour l'application. 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é par l'application dépasse 256 Mo, le système d'exploitation supprime les pages de mémoire inactives à l'aide de l'éviction et de l'échange.

Varier les budgets en fonction de l'état du processus

Une application nécessite différentes quantités de mémoire en fonction de sa visibilité par l'utilisateur :

  • Premier plan : le processus héberge une activité visible qui interagit avec l'utilisateur. Cet état a généralement l'empreinte la plus importante en raison de l'interface utilisateur et des graphismes actifs.
  • Perceptible : le processus est perceptible par l'utilisateur, mais n'héberge pas de fenêtre visible (par exemple, hébergement d'un service de premier plan de lecture multimédia, d'un téléchargement actif en arrière-plan, d'une navigation détaillée ou d'une méthode de saisie active).
  • Arrière-plan : le processus exécute des jobs, 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 qu'elles correspondent à 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>

Une clause de base sans android:state n'est pas obligatoire. Si vous ne spécifiez que des clauses spécifiques à un État (par exemple, android:state="background"), les autres États ne sont pas limités par un budget d'application. Lorsqu'elle est incluse, une clause sans android:state sert de remplacement par défaut pour les états non spécifiés (comme le premier plan), que les clauses plus restrictives suivantes remplacent lorsque l'application passe aux états perceptible ou background.

Lorsque vous dimensionnez les budgets pour chaque état, ciblez l'ensemble de travail actif de cet état plutôt que la mémoire résidente maximale (limite maximale RSS) :

  • Dimensionnez les budgets foreground pour un rendu fluide des frames : donnez à l'UI de premier plan suffisamment de marge pour son ensemble de travail actif afin que la récupération synchrone ne retarde pas le rendu des frames. Si votre application inclut un workflow autonome gourmand en mémoire (comme un éditeur d'images ou de vidéos haute résolution), envisagez de déplacer cette activité vers un processus dédié (voir Applications multiprocessus) afin que ses allocations soient budgétisées indépendamment et supprimées lorsque l'utilisateur quitte l'application.
  • Définissez des budgets background stricts pour les tâches sans interface graphique : un RSS élevé en arrière-plan dans la télémétrie de terrain reflète souvent les allocations d'UI conservées après avoir quitté le premier plan (voir Colocaliser un service lié avec une UI gourmande en mémoire et Libérer de la mémoire en réponse à des événements) ou des lectures de fichiers ponctuelles lors de l'indexation et de la synchronisation en arrière-plan. Étant donné que les tâches en arrière-plan sans interface graphique n'ont pas de délais d'exécution, l'éviction des pages de cache de fichiers ponctuelles et l'échange de pages de tas inactives vers la zRAM lorsqu'une synchronisation en arrière-plan dépasse brièvement son budget n'entraînent pas d'à-coups visibles par l'utilisateur, tout en empêchant les pics d'activité en arrière-plan d'évincer l'application au premier plan de l'utilisateur.

Correspondance entre les états du budget et les états du processus

La plate-forme mappe les états du budget du fichier manifeste aux états du processus d'exécution en fonction de RunningAppProcessInfo.importance, qu'Activity Manager calcule à partir des composants actifs du processus et de toutes les liaisons de service ou requêtes de fournisseur de contenu entrantes provenant d'autres applications :

  • foreground : interactions utilisateur actives et visibles, telles que l'hébergement d'une activité reprise, le maintien de l'état de l'application principale lorsque l'écran s'éteint, ou l'hébergement d'un service ou d'un fournisseur de contenu activement lié ou interrogé par l'application principale au premier plan.
  • perceptible : charges de travail perceptibles par l'utilisateur sans fenêtre visible, telles que la lecture de contenus multimédias actifs, la navigation détaillée, la capture de l'appareil photo ou du micro, les téléchargements actifs en arrière-plan, les services de synchronisation des données au premier plan ou les services liés par un client de premier plan à l'aide d'options telles que BIND_NOT_FOREGROUND (voir Contrôler l'héritage avec des options BIND). Bien que les métriques de la plate-forme interne puissent étiqueter certaines de ces charges de travail comme PROCESS_STATE_IMPORTANT_FOREGROUND, cette constante interne ne signifie pas qu'il existe une fenêtre visible et elle est régie par le budget perceptible.
  • background : tâches qui ne sont pas immédiatement perceptibles par l'utilisateur, telles que les tâches en arrière-plan, les alarmes, les broadcast receivers ou l'activité précédente après que l'utilisateur a quitté l'application.

Le tableau suivant montre comment les niveaux d'importance d'exécution sont mappés aux états du fichier manifeste :

Fichier manifeste android:state Importance de l'environnement d'exécution (RunningAppProcessInfo) Composants types
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Activité visible réactivée, application de premier plan avec l'écran verrouillé, service ou fournisseur de contenu lié par l'application de premier plan
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Lecture multimédia, navigation, téléchargements ou services de premier plan de synchronisation actifs ; service lié à des indicateurs de service de premier plan ou visibles
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
Jobs en arrière-plan, récepteurs, synchronisation en arrière-plan, activité précédente ou application Home en arrière-plan

Pour en savoir plus sur la façon dont les liaisons client élèvent un processus hôte de service ou de fournisseur, et sur la façon dont le processus revient à background ou cached une fois que le client se dissocie ou se déplace hors de l'écran, consultez Liaisons de service et états de processus.

Lorsqu'un processus passe à l'état mis en cache (IMPORTANCE_CACHED) sans composant actif, la plate-forme libère son budget de mémoire active et gère sa mémoire via la récupération standard des processus mis en cache.

Inspecter l'état et le budget du processus de votre application

Pour inspecter l'état et l'importance du processus actif de votre application pendant le développement, vous pouvez interroger Activity Manager à l'aide d'ADB :

adb shell dumpsys activity processes <package-name>

L'exemple abrégé suivant montre l'enregistrement du processus et les entrées de contrôle OOM pour une application exécutant un service de premier plan :

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

Dans ce résultat :

  • curProcState=4 et state: cur=FGS indiquent que le processus est à l'état de service de premier plan. Si votre application héberge une activité visible active, elle apparaît sous la forme TOP. S'il exécute un composant de premier plan important (comme un téléchargement ou une synchronisation), il apparaît comme IMPF.
  • Dans la table de contrôle OOM du processus, prcp indique que le processus est évalué sous le niveau de priorité perceptible, qui correspond au budget perceptible.

Pour inspecter le budget de mémoire actuellement appliqué et l'utilisation de la mémoire résidente :

adb shell dumpsys meminfo <package-name>

À partir d'Android 17 QPR2, ce résultat inclut une section Budget mémoire qui affiche le plafond de la limite active, la source de la limite (par exemple, AndroidManifest ou MemoryBudgetManager) et la mémoire résidente actuelle.

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> dans <processes>.

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 le budget de premier plan de 180 Mo. Lorsque l'utilisateur quitte l'application pendant 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" />

        <processes>
            <!-- Default application process (uses the package baseline budgets above) -->
            <process />

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

Lorsque vous déclarez un bloc <processes> dans votre fichier manifeste, incluez un élément <memory-budget> englobant directement sous <application>, et listez une entrée <process> pour chaque processus utilisé par les composants de votre fichier manifeste fusionné, y compris <process /> pour le processus d'application par défaut. Si vous souhaitez budgétiser uniquement des sous-processus spécifiques tout en laissant le reste de l'application à la limite système de l'appareil, déclarez une référence de package élevée, telle que <memory-budget android:maxMb="1000000" /> sous <application>.

L'utilisation de la mémoire dans un 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.

Ajuster les budgets pour les écrans haute densité

Pour les applications dont l'espace mémoire utilisé évolue considérablement en fonction du nombre de pixels à dessiner simultanément à l'écran (par exemple, une application de galerie photo qui met en cache des bitmaps dimensionnés pour l'écran), Android propose deux mécanismes alternatifs pour adapter les budgets de manière dynamique aux spécifications de l'écran :

  • Mise à l'échelle par bucket de densité d'affichage (android:additionalMbPerDensity) : ajoute des mégaoctets proportionnellement au carré du ratio de densité de l'affichage par rapport à mdpi (1,0x / 160 ppp), car les drawables matriciels 2D et les éléments d'interface utilisateur sont mis à l'échelle en largeur et en hauteur :

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

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

  • Mise à l'échelle en fonction de la résolution physique de l'écran (android:additionalBytesPerDisplayPixel) : ajoute des octets directement par pixel d'affichage physique (largeur × hauteur). Cette option est idéale pour les applications qui allouent des surfaces graphiques en plein écran, des tampons de rendu ou des caches de photos en résolution maximale, où la consommation de mémoire évolue directement en fonction du nombre de pixels d'affichage bruts plutôt que des buckets de densité de l'interface utilisateur :

    <!-- 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 × 2400 &approx; 2,59 millions de pixels), cela ajoute environ 41,4 Mo au budget de référence. Sur un écran 1440p (1440 × 3120 &approx; 4,49 millions de pixels), cela 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écialiser pour les facteurs de forme des appareils

Lorsque vous distribuez 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 dernière clause budgétaire applicable est celle qui est appliquée.

Comme le dernier budget applicable l'emporte, 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 cgroup Linux (qui exclut la mémoire partagée comme Zygote).

Attribut Format Par défaut Description
android:maxMb Entier (> 0) Obligatoire Limite de référence du budget de mémoire résidente en Mo.
android:state Enum Tous État du processus auquel ce budget s'applique : foreground, perceptible ou background. Si elle est omise, la clause sert de solution de secours pour tout état non spécifié.
android:additionalMbPerDensity Entier (≥ 0) 0 Mégaoctets supplémentaires multipliés par le carré du 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 Limite la clause aux appareils déclarant des fonctionnalités matérielles spécifiques : watch, automotive ou leanback.

API d'exécution (option dynamique secondaire)

La déclaration statique des budgets dans AndroidManifest.xml est la solution privilégiée pour presque toutes les applications. Toutefois, pour les applications avec des charges de travail dynamiques ou pour l'expérimentation d'exécution, Android fournit des API SDK et NDK d'exécution comme option secondaire.

L'API d'exécution vous permet d'effectuer les opérations suivantes :

  • Interrogez l'utilisation actuelle de la mémoire et les budgets effectifs.
  • Ajustez dynamiquement votre budget de 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 du SDK Android (MemoryBudgetManager)

Le service système MemoryBudgetManager est disponible pour les applications écrites en Kotlin et en Java à partir d'Android 17 QPR2 (version mineure du SDK, niveau d'API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Récupérer le service

Avant d'accéder à MemoryBudgetManager, vérifiez que l'appareil n'exécute pas une version antérieure à Android 17 QPR2 à l'aide de SDK_INT_FULL :

if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2) {
    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 strict au moment de l'exécution pour limiter la mémoire lors des 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 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()

Contrairement aux clauses du fichier manifeste <memory-budget>, qui changent automatiquement de budget chaque fois que le processus passe d'un état foreground, perceptible et background à un autre, un budget dynamique défini via MemoryBudgetManager applique un plafond unique jusqu'à ce que votre code le mette à jour ou l'efface. Si vous utilisez l'API d'exécution pour exécuter des tests sur le terrain ou gérer des limites spécifiques à l'état, mettez à jour ou effacez le budget dynamique lors des transitions de cycle de vie (par exemple, à l'aide de ProcessLifecycleOwner ou de rappels de cycle de vie d'activité, comme indiqué dans l'exemple d'éditeur d'images adaptatif).

Performances et idempotence

Lorsque vous utilisez MemoryBudgetManager au moment de l'exécution, tenez compte des caractéristiques suivantes :

  • Idempotence : toutes les API de mutation de budget sont idempotentes. Appeler clearProcessBudget() ou clearPackageBudget() de manière répétée ou lorsqu'aucun budget dynamique n'est actif est une opération sans effet et sans risque. De même, définir setProcessBudgetBytes() ou setPackageBudgetBytes() sur la même valeur de manière consécutive ne déclenche pas de reconfigurations redondantes du système.
  • Fréquence d'appel : la définition, la mise à jour ou l'effacement d'un budget effectue un appel interprocessus au service système pour mettre à jour le contrôleur de mémoire du système d'exploitation pour le processus. Bien qu'il soit léger, il n'est pas totalement sans frais. Appelez ces API lors des transitions de cycle de vie et des étapes de tâche (par exemple, lors de l'entrée ou de la sortie d'une activité, du démarrage ou de la fin du traitement en arrière-plan, ou du traitement des requêtes vocales). Évitez de les appeler dans des boucles serrées, des threads de traitement audio ou des routines de rendu par frame.
  • Fréquence des requêtes : les propriétés de requête processCurrentUsageBytes et packageCurrentUsageBytes récupèrent l'utilisation de la mémoire en direct à partir du service système. Bien que rapides, les requêtes doivent être appelées à la demande (par exemple, lors des transitions de tâches ou dans les rappels OnOverBudgetListener) plutôt que d'être interrogées dans des boucles continues.

Écouter les rappels de pression 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 de bitmaps 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 en cas de dépassement de budget :

  • Agir rapidement : les opérations de récupération doivent offrir un soulagement immédiat. Les calculs complexes effectués pendant les périodes de forte sollicitation dégradent les performances.
  • Évitez les allocations : n'allouez pas de nouveaux objets et ne démarrez pas 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 bitmaps volumineux, de tampons de rendu ou la fermeture de fichiers mappés en mémoire est beaucoup plus efficace 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 à partir d'Android 17 QPR2 (niveau d'API 37.2).

Configuration de 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 (or unconstrained baseline)
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 d'exécution

Les exemples suivants montrent comment implémenter les API d'exécution à l'aide de l'API du SDK Android (écrite en Kotlin) et de l'API NDK native (écrite en C++).

Exemple de SDK Android : éditeur d'images adaptatif

Cet exemple montre une application de retouche d'images (com.example.imageeditor) dont le fichier manifeste déclare un plafond de 256 Mo pour accueillir son canevas multicouche :

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

Lorsque l'utilisateur parcourt la galerie de vignettes légères, l'application utilise l'API du SDK Android en Kotlin pour réduire de manière dynamique son budget de processus à 96 Mo. Lorsque l'utilisateur ouvre le canevas de montage multicouche, l'application efface le budget dynamique pour restaurer le plafond complet de 256 Mo du fichier manifeste. Il enregistre également un OnOverBudgetListener pour supprimer les bitmaps d'aperçu mis en cache en cas de pression.

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

Exemple de NDK C++ : moteur 3D natif

Cet exemple montre un moteur de jeu C++ natif qui gère les budgets de mémoire en fonction du niveau de qualité graphique actif, en supposant que le fichier manifeste de l'application déclare un plafond de 512 Mo pour s'adapter aux graphismes de haute qualité (android:maxMb="512"). Le moteur resserre dynamiquement le budget pour les préréglages de qualité inférieure et utilise 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;
};