Mit Arbeitsspeicherbudgets für Apps können Apps ein eigenes Arbeitsspeicherbudget deklarieren. Das System wird dann angewiesen, die Arbeitsspeichernutzung zu reduzieren, wenn die App mehr als das festgelegte Budget verwendet. Dies ist besonders nützlich für System- und gebündelte Apps oder für Apps, die für Geräte mit begrenztem Arbeitsspeicher entwickelt wurden. In diesen Fällen kennt der Entwickler den erwarteten Arbeitssatz (den Arbeitsspeicher, den die App aktiv für die aktuelle Aktion des Nutzers benötigt) und möchte sicherstellen, dass die App nicht zu viele der gemeinsam genutzten RAM-Ressourcen des Systems verwendet.
Das Budget wird ausgeglichen, indem Speicherbereinigung und Auslagerung verwendet werden, um Speicherseiten zu entfernen, die in letzter Zeit nicht verwendet wurden. Dadurch wird der Speicherbedarf der App auf ihren aktuellen Arbeitssatz konzentriert. Wenn eine App das deklarierte Budget überschreitet, zielt das Betriebssystem speziell auf diese App ab:
- Bereinigte dateibasierte Seiten (z. B. inaktiver Code und zugeordnete Assets) werden zuerst entfernt, da sie bei Bedarf noch einmal aus dem Speicher gelesen werden können.
- Dirty-Seiten, die durch Dateien gesichert sind, werden in den Speicher zurückgeschrieben und entfernt.
- Anonyme Speicherseiten (z. B. Heap-Zuweisungen) werden komprimiert und in zRAM ausgelagert.
Solange das Working Set das Budget nicht überschreitet, funktioniert die App gut und verbraucht nicht mehr Arbeitsspeicher als im Budget festgelegt. Das Betriebssystem entfernt nicht genutzten Speicher und komprimiert inaktive Heap-Seiten in den Swap-Speicher, sodass die Speicherzuweisungen begrenzt bleiben, ohne den Prozess zu beenden.
Was passiert, wenn eine App ihr Budget überschreitet?
Wenn ein Arbeitsspeicherbudget überschritten wird, wird die App nicht beendet oder stürzt ab. Stattdessen sucht das Betriebssystem, wenn eine App mehr Arbeitsspeicher verwendet, als ihr Budget zulässt, nach Arbeitsspeicher, den die App seit einiger Zeit nicht mehr verwendet hat, und legt ihn sicher ab, bis er wieder benötigt wird. Dadurch wird Platz für neue Arbeitsspeicherzuweisungen der App geschaffen und die Gesamtspeichernutzung der App bleibt im Rahmen des Budgets.
Solange zwischen dem, was die App zu einem bestimmten Zeitpunkt benötigt (ihrem Arbeitssatz), und dem festgelegten Budget noch Spielraum ist, läuft die App normal weiter. Wenn eine App ein Budget festlegt, das kleiner als ihr Arbeitssatz ist, kann sich die Leistung der App zur Laufzeit verlangsamen.
Informationen dazu, wie das Betriebssystem den Arbeitsspeicher verfolgt und freigibt, finden Sie im Leitfaden zur Speicherarchitektur:
- Speicherfreigabe und -austausch: Hier wird erläutert, wie das Betriebssystem private anonyme Heap- und dateibasierte Seiten Ihrer App zuordnet und kalte Seiten freigibt, wenn das Budget überschritten wird.
- Grundlegende Konzepte: Hier wird erläutert, warum der gemeinsam genutzte Zygote-Speicher nicht im Budget Ihrer App enthalten ist und wie sich die Budgets für den residenten Speicher auf anonyme RSS + Swap in der Feldtelemetrie beziehen.
- Bitmaps und Arbeitsspeicher: Hier wird erläutert, wie GPU-gestützte Hardware-Bitmaps und DMA-BUF-Zuweisungen außerhalb des Heaps und des Arbeitsspeicherbudgets Ihrer App berücksichtigt werden.
Budgets im Android-Manifest deklarieren
Die Deklaration Ihrer Speicherbudgets in Ihrer AndroidManifest.xml ist die primäre und empfohlene Methode zum Definieren von Budgets. Es ist kein Laufzeitcode erforderlich, es wird sofort beim Start des Prozesses wirksam und bietet einen klaren Vertrag für das Betriebssystem.
<memory-budget>-Deklarationen werden auf Geräten mit Android 17 QPR2 (API-Level 37.2) und höher wirksam. In älteren Android-Versionen ignoriert der Manifest-Parser der Plattform nicht erkannte XML-Elemente, sodass Sie <memory-budget> verwenden können, ohne die Abwärtskompatibilität zu beeinträchtigen.
Ausgangsbudget deklarieren
Für die meisten Anwendungen reicht es aus, ein einzelnes Budget für die Anwendung zu definieren. Deklarieren Sie ein <memory-budget>-Element direkt im <application>-Tag:
<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>
Dadurch wird ein Budget von 256 MB für den residenten Arbeitsspeicher für alle Prozesse und Status des Pakets festgelegt. Wenn der Speicherbedarf der App 256 MB überschreitet, werden inaktive Speicherseiten vom Betriebssystem durch Entfernen und Auslagern gekürzt.
Budgets nach Prozessstatus variieren
Eine App benötigt je nach Sichtbarkeit für den Nutzer unterschiedlich viel Arbeitsspeicher:
- Vordergrund: Der Prozess hostet eine sichtbare Aktivität, die mit dem Nutzer interagiert. Dieser Zustand hat aufgrund der aktiven Benutzeroberfläche und Grafik in der Regel den größten Speicherbedarf.
- Wahrnehmbar: Der Prozess ist für den Nutzer wahrnehmbar, hostet aber kein sichtbares Fenster (z. B. Hosting eines Dienstes im Vordergrund für die Medienwiedergabe, eines aktiven Downloads im Hintergrund, einer detaillierten Routenführung oder einer aktiven Eingabemethode).
- Hintergrund: Der Prozess führt Hintergrundjobs, Empfänger oder Datensynchronisierungen aus. Es wird erwartet, dass der Ressourcenverbrauch minimal ist.
Sie können mehrere <memory-budget>-Klauseln deklarieren, um diese Status abzugleichen:
<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>
Eine Basisklausel ohne android:state ist nicht erforderlich. Wenn Sie nur bundesstaatspezifische Klauseln angeben (z. B. android:state="background"), bleiben andere Bundesstaaten von einem Anwendungskontingent unberührt. Wenn eine Klausel ohne android:state enthalten ist, dient sie als Standard-Fallback für nicht angegebene Zustände (z. B. Vordergrund). Nachfolgende, restriktivere Klauseln überschreiben diese, wenn die App in den Zustand perceptible oder background wechselt.
Wenn Sie die Budgets für die einzelnen Zustände festlegen, sollten Sie sich am aktiven Arbeitssatz des jeweiligen Zustands orientieren und nicht am maximalen residenten Speicher (RSS-Höchstwert):
foreground-Budgets für flüssiges Frame-Rendering dimensionieren: Gib der Benutzeroberfläche im Vordergrund genügend Spielraum für ihren aktiven Arbeitssatz, damit die synchrone Freigabe das Frame-Rendering nicht verzögert. Wenn Ihre App einen speicherintensiven eigenständigen Workflow enthält (z. B. einen Editor für hochauflösende Bilder oder Videos), sollten Sie diese Aktivität in einen dedizierten Prozess verschieben (siehe Apps mit mehreren Prozessen), damit die Zuweisungen unabhängig budgetiert und beim Beenden durch den Nutzer gelöscht werden.background-Budgets für Headless-Arbeit eng bemessen: Ein hoher RSS-Wert im Hintergrund in der Feldtelemetrie spiegelt oft UI-Zuweisungen wider, die nach dem Verlassen des Vordergrunds beibehalten werden (siehe Gebundenen Dienst mit einer speicherintensiven UI zusammenlegen und Speicher als Reaktion auf Ereignisse freigeben) oder einmalige Dateilesevorgänge während der Hintergrundindexierung und -synchronisierung. Da für die Hintergrundarbeit ohne Benutzeroberfläche keine Frame-Fristen gelten, führt das Entfernen von einmaligen Dateicache-Seiten und das Auslagern inaktiver Heap-Seiten in zRAM, wenn eine Hintergrundsynchronisierung kurzzeitig über ihr Budget hinausgeht, nicht zu sichtbaren Rucklern. Gleichzeitig wird verhindert, dass die App im Vordergrund durch Hintergrundspitzen entfernt wird.
Zuordnung von Budgetstatus zu Prozessstatus
Die Plattform ordnet die im Manifest angegebenen Budgetstatus den Laufzeitprozessstatus basierend auf RunningAppProcessInfo.importance zu. Diese werden vom Activity Manager sowohl aus den eigenen aktiven Komponenten des Prozesses als auch aus allen eingehenden Dienstbindungen oder Contentanbieter-Abfragen von anderen Apps berechnet:
foreground: Aktive, sichtbare Nutzerinteraktionen, z. B. das Hosten einer fortgesetzten Aktivität, das Beibehalten des obersten App-Status, wenn der Bildschirm ausgeschaltet wird, oder das Hosten eines Dienstes oder Contentanbieters, der aktiv von der obersten App im Vordergrund gebunden oder abgefragt wird.perceptible: Arbeitslasten, die für den Nutzer ohne sichtbares Fenster wahrnehmbar sind, z. B. aktive Medienwiedergabe, detaillierte Routenführung, Kamera- oder Mikrofonaufnahme, aktive Downloads im Hintergrund, Datensynchronisierung im Vordergrund oder Dienste, die von einem Vordergrund-Client mit Flags wieBIND_NOT_FOREGROUNDgebunden sind (siehe Vererbung mit BIND-Flags steuern). In internen Plattformmesswerten werden einige dieser Arbeitslasten möglicherweise alsPROCESS_STATE_IMPORTANT_FOREGROUNDbezeichnet. Diese interne Konstante bedeutet jedoch kein sichtbares Fenster und wird durch dasperceptible-Budget geregelt.background: Arbeit, die für den Nutzer nicht sofort erkennbar ist, z. B. Hintergrundjobs, Alarme, Übertragungsempfänger oder die vorherige Aktivität, nachdem der Nutzer die Seite verlassen hat.
In der folgenden Tabelle sehen Sie, wie die Wichtigkeitsstufen zur Laufzeit den Manifeststatus zugeordnet werden:
Manifest android:state |
Laufzeitbedeutung (RunningAppProcessInfo) |
Typische Komponenten |
|---|---|---|
foreground |
IMPORTANCE_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
Fortgesetzte sichtbare Aktivität, oberste App bei gesperrtem Bildschirm, Dienst oder Contentanbieter, der an die oberste App gebunden ist |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
Aktive Medienwiedergabe, Navigation, Downloads oder Synchronisierung von Vordergrunddiensten; Dienst mit Vordergrunddienst oder sichtbaren Flags gebunden |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
Hintergrundjobs, Empfänger, Hintergrundsynchronisierung, vorherige Aktivität oder Start-App im Hintergrund |
Weitere Informationen dazu, wie Clientbindungen einen Dienst- oder Anbieterhostprozess erhöhen und wie der Prozess nach dem Aufheben der Bindung oder dem Verschieben des Clients aus dem Bildschirm wieder in background oder cached übergeht, finden Sie unter Dienstbindungen und Prozessstatus.
Wenn ein Prozess ohne aktive Komponenten in den Cache-Status (IMPORTANCE_CACHED) wechselt, gibt die Plattform sein aktives Speicherbudget frei und verwaltet seinen Speicher über die standardmäßige Rückgewinnung von im Cache gespeicherten Prozessen.
Prozessstatus und Budget Ihrer App prüfen
Eine Möglichkeit, den aktiven Prozessstatus und die Wichtigkeit Ihrer App während der Entwicklung zu prüfen, besteht darin, den Activity Manager mit ADB abzufragen:
adb shell dumpsys activity processes <package-name>
Das folgende gekürzte Beispiel zeigt den Prozesseintrag und die OOM-Steuerungseinträge für eine App, die einen Dienst im Vordergrund ausführt:
ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
All known processes:
*APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
pid=3919
oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
hasStartedServices=true
mHasForegroundServices=true forcingToImportant=null
...
Process OOM control (48 total):
Proc #22: prcp F/S/FGS ---NFU-TI t: 0 3919:com.example.app/u0a123 (fg-service)
oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
state: cur=FGS set=FGS lastRss=0.00 lastCachedRss=0.00
In dieser Ausgabe gilt:
curProcState=4undstate: cur=FGSgeben an, dass sich der Prozess im Status „Dienst im Vordergrund“ befindet. Wenn in Ihrer App eine aktive sichtbare Aktivität gehostet wird, wird dies alsTOPangezeigt. Wenn eine wichtige Vordergrundkomponente ausgeführt wird (z. B. ein Download oder eine Synchronisierung), wirdIMPFangezeigt.- In der OOM-Steuerungstabelle für Prozesse gibt
prcpan, dass der Prozess unter der wahrnehmbaren Prioritätsstufe ausgewertet wird, die demperceptible-Budget entspricht.
So prüfen Sie das aktuell erzwungene Arbeitsspeicherbudget und die Nutzung des residenten Arbeitsspeichers:
adb shell dumpsys meminfo <package-name>
Ab Android 17 QPR2 enthält diese Ausgabe den Abschnitt Memory Budget, in dem die aktive Obergrenze, die Quelle der Begrenzung (z. B. AndroidManifest oder MemoryBudgetManager) und der aktuelle residente Speicher angezeigt werden.
Apps mit mehreren Prozessen
Wenn Ihre Anwendung ihre Arbeit auf mehrere Prozesse aufteilt, konfigurieren Sie dedizierte Prozessbudgets mit dem Tag <process> in <processes>.
Beispiel: Eine Musikstreaming-App (com.example.radio):
- Hauptprozess: Hostet die sichtbare Benutzeroberfläche und die Audiowiedergabe-Engine (
MediaSessionServicemit einemmediaPlayback-Dienst im Vordergrund). Wenn er sichtbar ist, wird er mit dem 180-MB-Vordergrundbudget ausgeführt. Wenn der Nutzer die App verlässt, während die Musik weiterläuft, wechselt der Prozess in den Statusperceptible. Hier reicht ein Budget von 64 MB für die Wiedergabe-Engine und den Audiopuffer aus. - Synchronisierungsprozess (
:sync): Dedizierter Prozess, der die Metadatensynchronisierung im Hintergrund und die Downloadindexierung ausführt. Da dieser Prozess nur im Hintergrund aktiv ist, müssen Siestate="background"nicht explizit deklarieren. Es gilt ein einzelnes Budget.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.radio">
<application
android:label="@string/app_name">
<!-- Package baseline: main process with UI and audio playback -->
<memory-budget android:maxMb="180" />
<!-- Tighter budget when audio plays in the background -->
<memory-budget
android:maxMb="64"
android:state="perceptible" />
<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>
Wenn Sie einen <processes>-Block in Ihrem Manifest deklarieren, listen Sie <process>-Einträge nur für die Prozesse auf, die ein <memory-budget> deklarieren. Wenn Sie nur bestimmte Unterprozesse budgetieren und den Rest der App auf dem Systemlimit für das Gerät belassen möchten, können Sie die <memory-budget>-Elemente direkt unter <application> weglassen.
Die Arbeitsspeichernutzung in einem Unterprozess wird sowohl auf das Prozessbudget als auch auf das Budget des übergeordneten Pakets angerechnet. Ein Prozess ist zur Laufzeit von Speicherknappheit betroffen, wenn er entweder sein Prozessbudget oder das Paketbudget überschreitet, je nachdem, welcher Schwellenwert zuerst erreicht wird.
Budgets für Displays mit hoher Pixeldichte skalieren
Für Anwendungen, deren Speicherbedarf stark mit der Anzahl der Pixel skaliert, die gleichzeitig auf dem Display dargestellt werden müssen, z. B. eine Fotogalerie-App, die Bitmaps in der Größe des Bildschirms im Cache speichert, bietet Android zwei alternative Mechanismen, um Budgets dynamisch mit den Displayspezifikationen zu skalieren:
Nach Displaydichte-Bucket skalieren (
android:additionalMbPerDensity): Fügt Megabyte proportional zum Quadrat des Dichteverhältnisses des Displays relativ zumdpi(1,0-fach / 160 dpi) hinzu, da 2D-Raster-Drawables und UI-Assets sowohl in der Breite als auch in der Höhe skaliert werden:<!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 --> <memory-budget android:maxMb="180" android:additionalMbPerDensity="16" />Auf einem
mdpi-Display (1,0x) beträgt das Budget 180 + 16 × 12 = 196 MB. Auf einemxxhdpi-Display (3,0x) wird das Budget auf 180 + 16 × 32 = 324 MB skaliert.Nach physischer Displayauflösung skalieren (
android:additionalBytesPerDisplayPixel): Fügt Bytes direkt pro physischem Displaypixel (Breite × Höhe) hinzu. Dies ist ideal für Anwendungen, die Vollbild-Grafikoberflächen, Renderpuffer oder Foto-Caches in voller Auflösung zuweisen, bei denen der Speicherverbrauch direkt mit der Anzahl der Rohanzeigepixel und nicht mit den UI-Dichtebereichen skaliert:<!-- 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" />Bei einem 1080p-Display (1080 × 2400 ≈ 2,59 Mio. Pixel) werden dem Basisbudget ≈ 41,4 MB hinzugefügt. Auf einem 1440p-Display (1440 × 3120 ≈ 4,49 Mio.Pixel) sind es ca.71,8 MB.
Diese beiden Attribute sind Alternativen. Wählen Sie das Attribut aus, das dem primären Skalierungsfaktor Ihrer App entspricht, und vermeiden Sie es, beide in derselben Klausel zu kombinieren.
Spezialisierung auf Geräteformfaktoren
Wenn du ein APK für Smartphones, Tablets und Wear OS bereitstellst, verwende das Attribut android:feature, um die Budgets für verschiedene Hardwareziele anzupassen.
Auf Wear OS-Smartwatches ist der Arbeitsspeicher begrenzt und die Benutzeroberfläche und der Funktionsumfang der App sind viel einfacher. Sie können ein knapperes Budget speziell für das Feature watch deklarieren:
<!-- 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" />
Lösungsregel: Die letzte anwendbare Klausel wird wirksam
Wenn Sie mehrere <memory-budget>-Elemente für eine Anwendung oder einen Prozess definieren, werden sie vom System in der Reihenfolge ausgewertet, in der sie im Manifest deklariert sind. Die zuletzt anwendbare Budgetklausel ist diejenige, die durchgesetzt wird.
Da das letzte anwendbare Budget gewinnt, ist die Reihenfolge wichtig. Platzieren Sie immer zuerst das allgemeinste Baseline-Budget, gefolgt von spezifischeren Überschreibungen (z. B. bundesstaatsspezifische oder hardwarespezifische Klauseln).
Referenz zu XML-Attributen
Alle Attribute für die Arbeitsspeichergröße werden in Megabyte (MB) angegeben und der Linux-cgroup-memory.current-Gebühr zugeordnet (ausgenommen gemeinsam genutzter Arbeitsspeicher wie Zygote).
| Attribut | Format | Standard | Beschreibung |
|---|---|---|---|
android:maxMb |
Ganzzahl (> 0) | Erforderlich | Das Budgetlimit für den residenten Speicher in MB. |
android:state |
Enum | Beliebig | Der Prozessstatus, für den dieses Budget gilt: foreground, perceptible oder background. Wenn sie weggelassen wird, dient die Klausel als Fallback für alle nicht angegebenen Status. |
android:additionalMbPerDensity |
Ganzzahl (≥ 0) | 0 |
Zusätzliche Megabyte multipliziert mit dem Quadrat des Verhältnisses der Anzeigendichte im Vergleich zu mdpi (1,0-fach). |
android:additionalBytesPerDisplayPixel |
Ganzzahl (≥ 0) | 0 |
Zusätzliche Byte, die pro physischem Displaypixel (Breite × Höhe) zugewiesen werden. Dies ist nützlich für Oberflächenpuffer und Bitmaps. |
android:feature |
String | Beliebig | Beschränkt die Klausel auf Geräte, die bestimmte Hardwarefunktionen deklarieren: watch, automotive oder leanback. |
Laufzeit-APIs (sekundäre dynamische Option)
Die statische Deklaration von Budgets in AndroidManifest.xml ist für fast alle Apps die bevorzugte Lösung. Für Anwendungen mit dynamischen Arbeitslasten oder für Laufzeitexperimente bietet Android jedoch Laufzeit-SDK- und NDK-APIs als sekundäre Option.
Mit der Laufzeit-API können Sie:
- Aktuelle Arbeitsspeichernutzung und effektive Budgets abfragen.
- Passen Sie Ihr Prozessbudget dynamisch nach unten an.
- Achten Sie auf Ereignisse, die das Budget überschreiten, um Caches proaktiv zu kürzen, bevor das Betriebssystem eine direkte Freigabe auslöst.
Android SDK API (MemoryBudgetManager)
Der Systemdienst MemoryBudgetManager ist für Apps, die in Kotlin und Java geschrieben wurden, ab Android 17 QPR2 (Minor SDK-Release, API-Level 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2) verfügbar.
Dienst abrufen
Bevor du auf MemoryBudgetManager zugreifst, prüfe, ob auf dem Gerät Android 17 QPR2 oder höher ausgeführt wird. Sieh dazu unter SDK_INT_FULL nach. Wenn die minSdk Ihrer App niedriger als 36 ist, prüfen Sie zuerst SDK_INT, damit Geräte mit Android 15 (API-Level 35) oder niedriger beim Lesen von SDK_INT_FULL keine NoSuchFieldError auslösen:
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)
}
Abfragenutzung und 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
Budgets dynamisch festlegen oder löschen
Sie können zur Laufzeit ein engeres Budget festlegen, um den Arbeitsspeicher bei einfachen Aufgaben zu begrenzen, oder es löschen, wenn die Aufgabe abgeschlossen ist:
// 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()
Im Gegensatz zu <memory-budget>-Manifestklauseln, die automatisch das Budget wechseln, wenn der Prozess zwischen den Zuständen foreground, perceptible und background wechselt, gilt für ein dynamisches Budget, das über MemoryBudgetManager festgelegt wird, eine einzige Obergrenze, bis Ihr Code sie aktualisiert oder löscht. Wenn Sie die Runtime API verwenden, um Feldexperimente auszuführen oder zustandsspezifische Limits zu verwalten, aktualisieren oder löschen Sie das dynamische Budget bei Lebenszyklusübergängen (z. B. mit ProcessLifecycleOwner oder Aktivitätslebenszyklus-Callbacks, wie im Beispiel für den adaptiven Bildeditor gezeigt).
Leistung und Idempotenz
Beachten Sie bei der Arbeit mit MemoryBudgetManager zur Laufzeit die folgenden Merkmale:
- Idempotenz: Alle APIs zur Budgetänderung sind idempotent. Das wiederholte Aufrufen von
clearProcessBudget()oderclearPackageBudget()oder wenn kein dynamisches Budget aktiv ist, ist ein sicherer No-Op. Ebenso löst das wiederholte Festlegen vonsetProcessBudgetBytes()odersetPackageBudgetBytes()auf denselben Wert keine redundanten Systemrekonfigurationen aus. - Aufrufhäufigkeit: Wenn ein Budget festgelegt, aktualisiert oder gelöscht wird, wird ein prozessübergreifender Aufruf an den Systemdienst gesendet, um den Speichercontroller des Betriebssystems für den Prozess zu aktualisieren. Sie ist zwar einfach, aber nicht völlig kostenlos. Rufe diese APIs während einzelner Lebenszyklusübergänge und Aufgabenmeilensteine auf, z. B. beim Starten oder Verlassen einer Aktivität, beim Starten oder Abschließen der Hintergrundverarbeitung oder beim Verarbeiten von Sprachanfragen. Vermeide es, sie in engen Schleifen, Audioprocessing-Threads oder Renderroutinen pro Frame aufzurufen.
- Abfragehäufigkeit: Die Abfrageattribute
processCurrentUsageBytesundpackageCurrentUsageBytesrufen die aktuelle Arbeitsspeichernutzung vom Systemdienst ab. Abfragen sind zwar schnell, sollten aber bei Bedarf aufgerufen werden (z. B. bei Aufgabenübergängen oder inOnOverBudgetListener-Callbacks), anstatt in kontinuierlichen Schleifen abgefragt zu werden.
Auf Callbacks für Drucküberschreitungen achten
Apps können einen Listener registrieren, um benachrichtigt zu werden, wenn die Arbeitsspeichernutzung den Budgetgrenzwert überschreitet. So kann die App eine proaktive Bereinigung auf App-Ebene durchführen (z. B. das Leeren von Bitmap-Caches im Arbeitsspeicher), während das Betriebssystem Arbeitsspeicher freigibt, um den Prozess oder das Paket innerhalb des Budgets zu halten:
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)
Best Practices für Callbacks bei Budgetüberschreitung:
- Hintergrund-
Looperverwenden: Übergeben Sie nach Möglichkeit einen Hintergrund-HandlerThreadLooperanregisterProcessOverBudgetListeneroderregisterPackageOverBudgetListener. Wenn Sie die Cache-Entfernung oder Dateibereinigung in einem Hintergrundthread ausführen, wird vermieden, dass es zu Konflikten mit dem Frame-Rendering im UI-Thread kommt, während der Prozess unter Speicherdruck steht. Reservieren SieLooper.getMainLooperfür Listener, die nur den UI-Status berühren, der an den Hauptthread gebunden ist. - Wiederholte Callbacks: Callbacks bei Budgetüberschreitung sind keine einmaligen Benachrichtigungen. Wenn die Nutzung das Budget überschreitet, fordert das Betriebssystem zuerst kalte Seiten zurück. Wenn der Druck anhält, fordert das System weiterhin Arbeitsspeicher zurück und ruft den Listener höchstens einmal pro Sekunde auf. Machen Sie den Callback idempotent. Wenn Ihre App nur eine einzelne Benachrichtigung benötigt oder eine längere Wartezeit zwischen den Bereinigungsdurchgängen gewünscht ist, rufen Sie
unregisterProcessOverBudgetListener(oderunregisterPackageOverBudgetListener) im Callback auf oder schützen Sie die Bereinigung mit einem Wartezeit-Zeitstempel oderAtomicBoolean. - Vor dem Verringern eines Budgets registrieren: Wenn die aktuelle Speichernutzung bereits ein neues Budget überschreitet, wenn Sie
setProcessBudgetBytesodersetPackageBudgetBytesaufrufen, wird ein Ereignis für die Budgetüberschreitung ausgelöst, sobald das neue Budget angewendet wird. Registrieren Sie den Listener, bevor Sie das Budget festlegen, damit Ihre App die erste Benachrichtigung über die Budgetüberschreitung nicht verpasst. - Schnell sein: Rückgewinnungsmaßnahmen müssen sofortige Erleichterung bringen. Komplexe Berechnungen während der Druckphase verschlechtern die Leistung.
- Zuweisungen vermeiden: Weisen Sie keine neuen Objekte zu und starten Sie keine neuen Threads innerhalb des Callbacks, da dies eine sofortige direkte Rückforderung durch das Betriebssystem auslösen kann.
- Auf ertragreiche Ziele konzentrieren: Das Entfernen großer Bitmaps, Renderpuffer oder das Schließen von speicherzugeordneten Dateien ist viel effektiver als das Freigeben vieler kleiner Objekte.
Native NDK API (<android/memory_budget_manager.h>)
Systemeigene Apps können die von libandroid.so bereitgestellte C NDK API ab Android 17 QPR2 (API-Level 37.2) verwenden.
CMake-Konfiguration
find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})
Header- und Abfragenutzung einbeziehen
#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
}
Natives Budget dynamisch konfigurieren
// 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();
Ereignisse mit hohem Arbeitsspeicherdruck überwachen
Das NDK bietet zwei Möglichkeiten, Speicherereignisse zu überwachen:
- High-Level Watcher (
AMemoryBudgetManager_Watcher_create): Überwacht Ereignisse auf einemALoopermit automatischer Entprellung. - Low-Level-Dateideskriptor:
AMemoryBudgetManager_getProcessMemoryPressureFdgibt einen nativen Dateideskriptor zurück, der direkt in eine benutzerdefinierteepoll-Engine-Schleife eingebunden werden kann.
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);
Binden Sie den Watcher an den ALooper eines Hintergrund-Arbeitsthreads und nicht an den Haupt-UI- oder Render-Thread, damit die Cache-Entfernung nicht mit dem Frame-Rendering in Konflikt gerät. Erstellen Sie den Watcher, bevor Sie ein Budget mit AMemoryBudgetManager_setProcessBudget oder AMemoryBudgetManager_setPackageBudget einschränken, damit Ihre Anwendung das erste Ereignis, bei dem das Budget überschritten wird, nicht verpasst.
Solange der Prozess oder das Paket das Budget überschreitet und das Betriebssystem weiterhin Speicher freigibt, ruft der Watcher den Callback wiederholt auf (höchstens einmal pro debounce_ms-Intervall). Machen Sie Ihren Callback idempotent oder rufen Sie AMemoryBudgetManager_Watcher_destroy auf (was innerhalb des Callbacks sicher ist), wenn Ihre App nur eine einmalige Benachrichtigung benötigt.
Beispiele für die Runtime API
In den folgenden Beispielen wird gezeigt, wie die Laufzeit-APIs mit der Android SDK API (in Kotlin geschrieben) und der Native NDK API (in C++ geschrieben) implementiert werden.
Android SDK-Beispiel: Adaptiver Bildeditor
In diesem Beispiel wird eine Bildbearbeitungs-App (com.example.imageeditor) gezeigt, deren Manifest eine Obergrenze von 256 MB für den mehrschichtigen Canvas deklariert:
<manifest ... >
<application ... >
<!-- Manifest ceiling accommodates the heaviest editing workload -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
Wenn der Nutzer die einfache Miniaturansichtsgalerie durchsucht, verwendet die App die Android SDK API in Kotlin, um das Prozessbudget dynamisch auf 96 MB zu reduzieren.
Wenn der Nutzer den Canvas für die Bearbeitung mehrerer Ebenen öffnet, löscht die App das dynamische Budget, um die volle Manifest-Obergrenze von 256 MB wiederherzustellen. Außerdem wird ein OnOverBudgetListener in einem Hintergrund-HandlerThread
Looper registriert, bevor das Galeriebudget angewendet wird, um im Cache gespeicherte Vorschaubitmaps bei Bedarf zu entfernen, ohne den UI-Thread zu blockieren.
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"
}
}
NDK-C++-Beispiel: Native 3D-Engine
In diesem Beispiel wird eine native C++-Spiel-Engine gezeigt, die Speicherbudgets basierend auf der aktiven Grafikqualität verwaltet. Dabei wird davon ausgegangen, dass im Manifest der Anwendung eine Obergrenze von 512 MB für Grafiken mit hoher Qualität (android:maxMb="512") deklariert ist. Die Engine startet AMemoryBudgetManager_Watcher_create in der ALooper eines Hintergrund-Worker-Threads, bevor das Budget für Voreinstellungen mit niedrigerer Qualität eingeschränkt wird. Wenn das Budget überschritten wird, werden Textur-Mipmaps entladen.
#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;
};