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 :
- 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.
- Les pages modifiées sauvegardées dans un fichier sont réécrites dans l'espace de stockage et supprimées.
- 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 :
- Récupération de mémoire et échange : explique comment l'OS impute à votre application les pages de tas anonymes privées et les pages de fichiers, et récupère les pages froides lorsque le budget est dépassé.
- Concepts fondamentaux : explique pourquoi la mémoire Zygote partagée est exclue du budget de votre application et comment les budgets résidents sont liés à la RSS anonyme + swap dans la télémétrie de champ.
- Bitmaps et mémoire : explique comment les bitmaps matériels et les allocations DMA-BUF basés sur le GPU sont comptabilisés en dehors du tas de mémoire et du budget de mémoire de votre application.
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
foregroundpour 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
backgroundstricts 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.
Pour obtenir un workflow par étapes, consultez Mesurer votre ensemble de travail et calibrer les budgets.
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 queBIND_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 commePROCESS_STATE_IMPORTANT_FOREGROUND, cette constante interne ne signifie pas qu'il existe une fenêtre visible et elle est régie par le budgetperceptible.background: travail qui n'est pas immédiatement perceptible par l'utilisateur, comme les tâches en arrière-plan, les alarmes et les broadcast receivers.
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_FOREGROUNDIMPORTANCE_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_SERVICEIMPORTANCE_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_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
Tâches en arrière-plan, récepteurs, synchronisation 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=4etstate: cur=FGSindiquent 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 formeTOP. S'il exécute un composant de premier plan important (comme un téléchargement ou une synchronisation), il apparaît commeIMPF.- Dans la table de contrôle OOM du processus,
prcpindique que le processus est évalué sous le niveau de priorité perceptible, qui correspond au budgetperceptible.
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) :
- Processus principal : héberge l'UI visible et le moteur de lecture audio (
MediaSessionServiceavec un service de premier planmediaPlayback). 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'étatperceptible, où un budget de 64 Mo est suffisant pour le moteur de lecture et le tampon audio. - 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 explicitementstate="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>
<!-- 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, ne listez les entrées <process> que pour les processus qui déclarent un <memory-budget>. 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, vous pouvez omettre les éléments <memory-budget> directement 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 écranxxhdpi(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 ≈ 2,59 millions de pixels), cela ajoute environ 41,4 Mo au budget de référence. Sur un écran 1440p (1440 × 3120 ≈ 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. |
Mesurer votre ensemble de travail et calibrer les budgets
Un bon budget se situe juste au-dessus de l'ensemble de travail de votre application pour un état donné, et non à son pic sans contrainte. Pour trouver ce point, exécutez un parcours utilisateur réaliste, réduisez le budget étape par étape et observez le moment où votre application commence à en payer le prix.
Comprendre la différence entre l'ensemble de travail et la mémoire résidente
Sans budget, l'empreinte résidente de votre application (memory.current) contient deux types de mémoire :
- L'ensemble de travail : le tas de mémoire Java, Kotlin et natif, et les pages de fichiers actives (code DEX, bibliothèques natives et éléments) que le parcours utilisateur actuel touche à plusieurs reprises.
- Slack froid : caches de bitmap des écrans précédents, tampons de démarrage ponctuels, slack de tas restant après une rafale d'allocation et pages de fichiers lues une fois et jamais modifiées.
Un budget à votre pic sans contrainte ne récupère jamais la marge de manœuvre inutilisée. Un budget inférieur à votre ensemble de travail amène le noyau à échanger des pages de tas et à supprimer des pages de code que votre application réintègre immédiatement. Ces erreurs de page de l'ensemble de travail consomment du temps CPU et de la batterie, et une fois qu'elles arrivent sur le thread UI, elles consomment également des images.
Vous recherchez le point d'inflexion : le budget le plus bas où la récupération ne supprime que l'espace inutilisé froid, de sorte que les défauts de page, le temps CPU et les temps de rendu restent les mêmes que sans budget.
Préparer votre environnement de test
Configurez un test reproductible qui se comporte comme en production :
- Calibrez la même version de publication optimisée pour R8 et non débogable que celle que vous livrez. Les versions de débogage utilisent plus de mémoire. Un budget ajusté sur une version de débogage est donc trop élevé pour la version finale.
- Effectuez des tests sur un appareil ou un émulateur exécutant Android 17 QPR2 (niveau d'API 37.2) ou version ultérieure. Il est préférable d'utiliser un build
userdebugou une image d'émulateur sans Google Play, car certains appareils ne vous permettent de lire les fichiers cgroup dans les sections suivantes qu'après l'exécution deadb root. - Scriptez un parcours utilisateur de 30 à 60 secondes avec Jetpack Macrobenchmark, UI Automator ou
adb shell input, une fois pour chaque état que vous prévoyez de budgétiser. Une application inactive ne vous dit rien sur son ensemble de travail.foreground: faites défiler des listes denses et des grilles multimédias, passez d'une destination principale à une autre, ouvrez des menus et des boîtes de dialogue, et lancez des animations.perceptible: quittez la fenêtre visible pendant qu'un travail perceptible se poursuit (par exemple, la lecture audio ou un téléchargement), laissez-le s'exécuter pendant 10 à 15 secondes, puis revenez à l'application.background: déclenchez une synchronisation, une tâcheWorkManager(par exemple, avecadb shell cmd jobscheduler run -f <package-name> <job-id>), un service démarré ou un broadcast receiver lorsque l'application n'a pas d'UI visible.
Mesurez un budget background pendant l'exécution du travail en arrière-plan. Une fois le processus mis en cache, la plate-forme libère son budget (voir Correspondance entre les états du budget et les états du processus). Un processus inactif mis en cache ne vous indique donc rien sur votre budget background.
Mesurez votre point de départ
Commencez par des exécutions sans budget, afin de savoir à quoi ressemble un comportement normal.
Utilisez un build sans déclarations <memory-budget> et démarrez chaque exécution à partir d'un nouveau processus (adb shell am force-stop <package-name>).
Chaque processus d'application possède son propre contrôleur de mémoire cgroup v2. Recherchez son chemin d'accès à partir de la ligne 0:: de /proc/<PID>/cgroup au lieu de créer vous-même le chemin d'accès, car la mise en page du cgroup peut différer d'un appareil à l'autre. Transmettez chaque commande d'appareil à adb shell sous la forme d'une chaîne entre guillemets simples afin que l'interface système de l'appareil voie les guillemets :
PID=$(adb shell pidof -s <package-name>)CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")adb shell "cat $CG/memory.current $CG/memory.swap.current"adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"
La lecture de ces fichiers est peu coûteuse et ne perturbe pas votre application. Vous pouvez donc les échantillonner toutes les secondes pendant une exécution :
memory.current: mémoire résidente facturée au processus, en octets. Il s'agit du nombre que<memory-budget>limite.memory.swap.current: mémoire compressée dans l'espace d'échange zRAM, en octetsanonetfiledansmemory.stat: mémoire anonyme résidente et mémoire sauvegardée dans un fichier, en octetsworkingset_refault_anonetworkingset_refault_filedansmemory.stat: nombre cumulé de pages échangées ou supprimées dont votre application avait à nouveau besoin. Ces compteurs sont le principal indicateur qu'un budget est trop serré.
Si vous obtenez une erreur d'autorisation, exécutez adb root et réessayez. Si vous ne pouvez pas obtenir l'accès racine, adb shell dumpsys meminfo <package-name> vous donne une vue plus grossière. Dans App Summary, Java Heap, Native Heap et Stack sont principalement de la mémoire anonyme, et Code est sauvegardé dans un fichier. Les bitmaps logiciels sont comptabilisés dans Native Heap et votre budget, contrairement aux bitmaps matériels et à Graphics. N'exécutez pas dumpsys meminfo en boucle pendant que vous mesurez le temps de rendu. Il force une récupération de mémoire dans votre application et parcourt tous ses mappages de mémoire, ce qui modifie ce que vous essayez de mesurer.
Budgets candidats pour le balayage
Vous pouvez tester des budgets candidats sans recompiler votre application. Utilisez am memory-limiter manual pour définir un budget pour un processus en cours d'exécution et none pour le supprimer :
adb shell am memory-limiter manual <PID> 120MBadb shell am memory-limiter manual <PID> none
Un remplacement manuel dure pendant toute la durée de vie de cette instance de processus, à travers les changements d'état du processus. Par conséquent, effacez-le ou redémarrez le processus avant de continuer.
Il définit également la limite stricte du processus (memory.max) sur la même valeur. Ainsi, un budget candidat inférieur à l'ensemble de travail de votre application peut entraîner l'arrêt du processus au lieu de simplement le ralentir. Considérez cela comme un budget non respecté.
Vous pouvez également effectuer un balayage à partir d'un build de benchmark en lisant une valeur candidate à partir d'un argument d'instrumentation et en définissant MemoryBudgetManager.processBudgetBytes. Un budget d'exécution ne peut que resserrer un budget de fichier manifeste. Par conséquent, effectuez un balayage sur un build sans déclarations <memory-budget>. Il reste également en vigueur lors des changements d'état du processus jusqu'à ce que vous appeliez clearProcessBudget.
Commencez par la valeur memory.current la plus élevée de votre référence et diminuez-la de 10 à 15 Mo à la fois. Pour chaque budget candidat :
- Démarrez un nouveau processus, amenez-le à l'état cible et appliquez le budget.
- Exécutez le parcours utilisateur une fois pour l'échauffement. L'application d'un budget supprime une fois les pages de démarrage à froid, et vous souhaitez mesurer l'état stable plutôt que cette transition.
Réinitialisez les statistiques de frames, puis enregistrez les compteurs de défauts de page de départ et le temps CPU du processus (
utimeetstime, en cycles d'horloge) :adb shell dumpsys gfxinfo <package-name> resetadb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"Exécutez la passe mesurée de votre parcours utilisateur.
Enregistrez à nouveau
memory.current,anon,file, les compteurs de refault et le temps CPU, puis collectez les statistiques de frame.dumpsys gfxinfopeut afficher les mêmes statistiques plusieurs fois. Prenez donc la première correspondance, qui couvre l'ensemble de l'application :adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'
Répétez chaque candidat au moins trois fois et entrelacez les candidats plutôt que d'exécuter toutes les répétitions d'un candidat l'une après l'autre. Comparez chaque résultat à l'écart de vos exécutions de référence, et non à une seule exécution.
Lire les résultats
À mesure que le budget diminue, votre application passe par trois régimes :
- Au-dessus du genou :
memory.currentsuit le budget en supprimant les pagesfilefroides et le tas inactif.anonreste stable, les refaults restent proches de la référence, et le temps CPU et les temps de rendu ne bougent pas. C'est là que vous voulez être. - Sous le genou : les pages froides ont disparu, de sorte que le noyau commence à échanger les pages de tas que votre application utilise toujours. Les défauts de page augmentent de centaines ou de milliers de pages par exécution, et le temps CPU augmente. Les temps de rendu peuvent à peine bouger sur un appareil de test inactif, mais votre application consacre désormais du temps CPU et de la batterie à compresser et décompresser son propre ensemble de travail.
- Bien en dessous du genou : les erreurs de récupération et d'échange se produisent sur le thread UI et
RenderThread, et les à-coups, les temps de rendu et les temps de reprise à chaud augmentent.
Par rapport à votre référence sans contrainte, un budget candidat est accepté s'il remplit toutes les conditions suivantes, qui constituent un point de départ raisonnable :
- Le processus survit à l'exécution
- Les deltas de Refault restent proches de la référence
- Le temps CPU du processus n'augmente pas de plus de 5 %
- Les images saccadées n'augmentent pas de plus de 1 point de pourcentage
- Le temps de rendu au 95e centile n'augmente pas de plus de 3 ms
- Le temps de reprise à chaud (
TotalTimeà partir deadb shell am start -W) n'augmente pas de plus de 15 % pour les budgetsperceptibleetbackground.
Le point d'inflexion est le budget candidat le plus bas qui est accepté. Par exemple, voici un balayage au premier plan pour une application hypothétique avec un pic non limité de 138 Mo :
| Budget du candidat | memory.current |
anon |
file |
Refaults (pages) | Temps de rendu au 95e centile | Régime |
|---|---|---|---|---|---|---|
| Aucun | 138 Mo | 75 Mo | 63 Mo | 0 | 10,5 ms | Référence |
| 125 Mo | 121 Mo | 74 Mo | 47 Mo | 10 | 10,5 ms | Au-dessus du genou |
| 110 Mo | 110 Mo | 75 Mo | 35 Mo | 15 | 10,8 ms | Genou |
| 95 Mo | 95 Mo | 74 Mo | 21 Mo | 176 | 11,2 ms | Sous le genou |
| 85 Mo | 85 Mo | 68 Mo | 17 Mo | 2 050 | 12,8 ms | Sous le genou |
| 70 Mo | 70 Mo | 54 Mo | 16 Mo | 8 771 | 16,5 ms | Bien en dessous du genou |
Définissez votre budget
Définissez android:maxMb de 10 % à 20 % au-dessus du seuil, de sorte que les différences de données utilisateur, de paramètres régionaux et de pics d'allocation ne fassent pas passer votre application en dessous de ce seuil. Dans l'exemple précédent, le coude se situe à 110 Mo, donc 125 Mo est un bon choix.
Ensuite, installez un build de version avec les nouvelles déclarations <memory-budget>, exécutez à nouveau votre parcours utilisateur et vérifiez que les critères de réussite sont toujours respectés. dumpsys
meminfo affiche le budget actif et sa source dans la section "Budget de mémoire".
Valider les budgets non exécutés au premier plan avec un retour à chaud
Un budget perceptible ou background serré peut sembler correct lorsque votre application est dans cet état, mais vous coûter cher lorsque l'utilisateur revient. Testez également le trajet retour :
- Exécutez le flux de premier plan, déplacez l'application vers l'état cible et appliquez le budget candidat avec
am memory-limiter manual. - Laissez la tâche perceptible ou en arrière-plan s'exécuter pendant 10 à 15 secondes.
- Effacez le remplacement (
adb shell am memory-limiter manual <PID> none), puis ramenez immédiatement l'application avecadb shell am start -Wet enregistrezTotalTime. Si vous définissez le candidat viaMemoryBudgetManager, appelez plutôtclearProcessBudgetavant que l'application ne renvoie un résultat. - Exécutez une courte interaction au premier plan, puis vérifiez les erreurs de page et les statistiques de frames par rapport aux critères de réussite.
Choisissez le budget le plus bas qui réussit les deux tests lorsque l'application est dans cet état et lors du retour à chaud, puis ajoutez la même marge de 10 % à 20 %.
Calibrer la mise à l'échelle de l'écran
Si vous utilisez android:additionalBytesPerDisplayPixel, recherchez le point d'inflexion de premier plan sur deux écrans de résolutions différentes, par exemple 1 080 × 2 400 (environ 2,59 millions de pixels) et 1 440 × 3 120 (environ 4,49 millions de pixels).
Divisez la différence entre les deux coudes, en octets, par la différence du nombre de pixels pour obtenir android:additionalBytesPerDisplayPixel. Soustrayez cette quantité par pixel pour l'écran plus petit de son point d'inflexion pour obtenir la base indépendante de la résolution, puis ajoutez de la marge pour obtenir android:maxMb.
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 exécute Android 17 QPR2 ou une version ultérieure en consultant SDK_INT_FULL. Si le minSdk de votre application est inférieur à 36, vérifiez d'abord SDK_INT pour que les appareils équipés d'Android 15 (niveau d'API 35) ou d'une version antérieure ne génèrent pas d'NoSuchFieldError lors de la lecture de SDK_INT_FULL :
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
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()ouclearPackageBudget()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éfinirsetProcessBudgetBytes()ousetPackageBudgetBytes()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
processCurrentUsageBytesetpackageCurrentUsageBytesré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 rappelsOnOverBudgetListener) 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 vidant les caches de bitmaps en mémoire) pendant que le système d'exploitation récupère de la mémoire pour maintenir le processus ou le package dans son budget :
val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
// Proactively evict thread-safe caches on the background Looper
imageTileCache.evictAll()
}
// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)
// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)
Bonnes pratiques pour les rappels en cas de dépassement de budget :
- Utiliser un
Looperen arrière-plan : transmettez unHandlerThreadLooperen arrière-plan àregisterProcessOverBudgetListenerouregisterPackageOverBudgetListenerchaque fois que possible. L'exécution de l'éviction du cache ou du nettoyage des fichiers sur un thread d'arrière-plan évite d'entrer en conflit avec l'affichage des images sur le thread UI lorsque le processus est soumis à une pression de mémoire. RéservezLooper.getMainLooperaux écouteurs qui ne touchent qu'à l'état de l'UI lié au thread principal. - Attendez-vous à des rappels répétés : les rappels de dépassement de budget ne sont pas des notifications ponctuelles. Lorsque l'utilisation dépasse le budget, le système d'exploitation récupère d'abord les pages froides. Si la pression persiste, le système continue de récupérer de la mémoire et d'appeler votre écouteur au maximum une fois par seconde. Rendez votre rappel idempotent. Si votre application n'a besoin que d'une seule notification ou si vous souhaitez un délai de refroidissement plus long entre les passes de nettoyage, appelez
unregisterProcessOverBudgetListener(ouunregisterPackageOverBudgetListener) dans le rappel, ou protégez votre nettoyage avec un code temporel de refroidissement ouAtomicBoolean. - Enregistrez-vous avant de réduire un budget : si l'utilisation actuelle de la mémoire dépasse déjà un nouveau budget lorsque vous appelez
setProcessBudgetBytesousetPackageBudgetBytes, un événement de dépassement de budget est déclenché dès que le nouveau budget est appliqué. Enregistrez votre écouteur avant de définir le budget afin que votre application ne manque pas la notification initiale 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 :
- Observateur de haut niveau (
AMemoryBudgetManager_Watcher_create) : surveille les événements sur unALooperavec un anti-rebond automatique. - Descripteur de fichier de bas niveau :
AMemoryBudgetManager_getProcessMemoryPressureFdrenvoie un descripteur de fichier natif qui peut être intégré directement dans une boucle de moteurepollpersonnalisé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 a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
background_looper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000 /* debounce_ms */,
&onMemoryPressure,
NULL /* userdata */
);
// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);
Associez l'observateur au ALooper d'un thread de travail en arrière-plan, plutôt qu'à votre UI principale ou à votre thread de rendu, afin que l'éviction du cache n'entre pas en conflit avec l'affichage des frames. Créez l'observateur avant de réduire un budget avec AMemoryBudgetManager_setProcessBudget ou AMemoryBudgetManager_setPackageBudget afin que votre application ne manque pas l'événement initial de dépassement de budget.
Tant que le processus ou le package dépasse le budget et que le système d'exploitation continue de récupérer de la mémoire, l'observateur appelle le rappel de manière répétée (au maximum une fois par intervalle debounce_ms). Rendez votre rappel idempotent ou appelez AMemoryBudgetManager_Watcher_destroy (qui peut être appelé en toute sécurité depuis le rappel) si votre application n'a besoin que d'une notification ponctuelle.
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 sur un HandlerThread
Looper en arrière-plan avant d'appliquer le budget de la galerie pour supprimer les bitmaps d'aperçu mis en cache sous pression sans bloquer le thread UI.
package com.example.imageeditor.ui
import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.os.HandlerThread
import android.util.Log
import android.util.LruCache
class ImageEditorActivity : Activity() {
private lateinit var budgetManager: MemoryBudgetManager
private lateinit var cleanupThread: HandlerThread
// Thread-safe 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)
cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
}
override fun onStart() {
super.onStart()
// Register listener on a background Looper before tightening the budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
// Constrain memory during lightweight gallery browsing
applyGalleryBudget()
}
override fun onStop() {
super.onStop()
budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
}
override fun onDestroy() {
cleanupThread.quitSafely()
super.onDestroy()
}
/**
* 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 gérant 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 démarre AMemoryBudgetManager_Watcher_create sur le ALooper d'un thread de travail en arrière-plan avant de réduire le budget pour les préréglages de qualité inférieure, et décharge 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;
}
// Call on a background worker ALooper before tightening the budget with setQualityBudget
bool startMonitoring(ALooper* backgroundLooper) {
if (!backgroundLooper) return false;
// Monitor process budget events, debounced to at most once every 1000ms
mWatcher = AMemoryBudgetManager_Watcher_create(
backgroundLooper,
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, idempotent, 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;
};