App-Arbeitsspeicherbudgets festlegen

Mit App-Arbeitsspeicherbudgets können Apps ein Arbeitsspeicherbudget für sich selbst deklarieren. Das System wird dann angewiesen, den Arbeitsspeicherverbrauch 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 auf Geräte mit wenig Arbeitsspeicher ausgerichtet sind. In diesen Fällen kennt der Entwickler den erwarteten Arbeitsspeicher-Working-Set (den Arbeitsspeicher, den die App aktiv für die aktuelle Nutzeraktivität benötigt) und möchte sicherstellen, dass die App nicht zu viele der gemeinsamen RAM-Ressourcen des Systems verwendet.

Das Budget wird durch Speicherauslagerung und ‑tausch ausgeglichen, um Speicherseiten zu entfernen, die in letzter Zeit nicht verwendet wurden. So wird der Speicherbedarf der App auf das aktuelle Arbeitsset konzentriert. Wenn eine App ihr deklariertes Budget überschreitet, zielt das Betriebssystem darauf ab, Ressourcen speziell für diese App zurückzugewinnen:

  1. Bereinigte dateibasierte Seiten (z. B. inaktiver Code und zugeordnete Assets) werden zuerst entfernt, da sie bei Bedarf aus dem Speicher neu gelesen werden können.
  2. Dirty file-backed pages werden in den Speicher zurückgeschrieben und entfernt.
  3. Anonyme Speicherseiten (z. B. Heap-Zuweisungen) werden komprimiert und in den zRAM-Swap-Speicher verschoben.

Solange das Working Set das Budget nicht überschreitet, funktioniert die App gut und verbraucht nicht mehr Arbeitsspeicher als im festgelegten Budget enthalten ist. Das Betriebssystem entfernt nicht verwendeten Arbeitsspeicher und komprimiert inaktive Heap-Seiten, um sie auszulagern. So bleiben Speicherzuweisungen begrenzt, ohne dass der Prozess beendet wird.

Was passiert, wenn eine App ihr Budget überschreitet?

Wenn eine App das Arbeitsspeicherbudget überschreitet, wird sie nicht vom System beendet oder stürzt ab. Stattdessen sucht das Betriebssystem nach Arbeitsspeicher, der von der App seit einiger Zeit nicht mehr verwendet wurde, und legt ihn sicher ab, bis er wieder benötigt wird. Dadurch wird Platz für neue Arbeitsspeicherzuweisungen für die App geschaffen und die gesamte Arbeitsspeichernutzung der App bleibt im Rahmen des Budgets.

Solange die App zu einem bestimmten Zeitpunkt weniger Arbeitsspeicher benötigt als das festgelegte Budget, läuft sie normal weiter. Wenn für eine App ein Budget festgelegt wird, das kleiner als ihr Arbeitsset ist, kann sich die Leistung der App zur Laufzeit verlangsamen.

Informationen dazu, wie das Betriebssystem Arbeitsspeicher zuweist und freigibt, finden Sie im Leitfaden zur Arbeitsspeicherarchitektur:

Budgets im Android-Manifest deklarieren

Die Deklaration Ihrer Speicherbudgets in Ihrem AndroidManifest.xml ist die primäre und empfohlene Methode zum Definieren von Budgets. Es ist kein Laufzeitcode erforderlich, die Änderung wird sofort beim Start des Prozesses wirksam und es wird ein klarer Vertrag für das Betriebssystem bereitgestellt.

<memory-budget>-Deklarationen werden auf Geräten mit Android 17 QPR2 (API-Level 37.2) und höher wirksam. In niedrigeren Android-Versionen werden unbekannte XML-Elemente vom Parser für das Plattformmanifest sicher ignoriert. Sie können <memory-budget> also verwenden, ohne die Abwärtskompatibilität zu beeinträchtigen.

Ausgangsbudget deklarieren

Für die meisten Apps 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 Auslagerung und Tausch entfernt.

Budgets nach Prozessstatus variieren

Eine App benötigt je nach Sichtbarkeit für Nutzer unterschiedlich viel Speicherplatz:

  • Vordergrund: Der Prozess hostet eine sichtbare Aktivität, die mit dem Nutzer interagiert. Dieser Status hat in der Regel den größten Speicherbedarf, da die Benutzeroberfläche und Grafiken aktiv sind.
  • Wahrnehmbar: Der Prozess ist für den Nutzer wahrnehmbar, es wird aber kein sichtbares Fenster gehostet (z. B. ein Dienst im Vordergrund für die Medienwiedergabe, ein aktiver Hintergrunddownload, eine Turn-by-Turn-Navigation oder eine aktive Eingabemethode).
  • Hintergrund: Der Prozess führt Hintergrundjobs, Empfänger oder Datensynchronisierungen aus. Der Ressourcenverbrauch sollte minimal sein.

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 bundesstaatsspezifische Klauseln angeben (z. B. android:state="background"), bleiben andere Bundesstaaten ohne App-Budget. Wenn eine Klausel ohne android:state enthalten ist, dient sie als Standard-Fallback für nicht angegebene Status (z. B. „foreground“). Nachfolgende restriktivere Klauseln überschreiben sie, wenn die App in den Status perceptible oder background wechselt.

Wenn Sie Budgets für die einzelnen Bundesstaaten festlegen, sollten Sie auf den aktiven Arbeitsbereich des jeweiligen Bundesstaats und nicht auf den maximalen residenten Speicher (RSS-Höchststand) abzielen:

  • Budgets für reibungsloses Rendern von Frames foreground festlegen: Geben Sie der Benutzeroberfläche im Vordergrund genügend Spielraum für ihr aktives Arbeitsset, damit das synchrone Zurückfordern das Rendern von Frames 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 eigenen Prozess verschieben (siehe Apps mit mehreren Prozessen). Die Zuweisungen werden dann unabhängig budgetiert und beendet, wenn der Nutzer die App schließt.
  • Größenbudgets backgroundfür Headless-Vorgänge eng fassen: Ein hoher Hintergrund-RSS in der Feldtelemetrie spiegelt oft UI-Zuweisungen wider, die nach dem Verlassen des Vordergrunds beibehalten werden (siehe Gebundenen Dienst mit einer speicherintensiven Benutzeroberfläche zusammenlegen und Arbeitsspeicher 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-Deadlines gelten, führt das Auslagern von einmaligen Dateicache-Seiten und das Swappen von inaktiven Heap-Seiten in zRAM, wenn bei einer Hintergrundsynchronisierung kurzzeitig das Budget überschritten wird, nicht zu für den Nutzer sichtbaren Rucklern. Gleichzeitig wird verhindert, dass durch Hintergrundspitzen die im Vordergrund ausgeführte App des Nutzers beendet wird.

Zuordnung von Budgetstatus zu Prozessstatus

Die Plattform ordnet Manifestbudgetstatus basierend auf RunningAppProcessInfo.importance Laufzeitprozessstatus zu. wird vom Activity Manager sowohl aus den eigenen aktiven Komponenten des Prozesses als auch aus allen eingehenden Dienstbindungen oder Contentanbieter anfragen anderer Apps berechnet:

  • foreground: Aktive, sichtbare Nutzerinteraktionen, z. B. das Hosten einer fortgesetzten Activity, das Beibehalten des Status der obersten App, 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, Aufnahme über Kamera oder Mikrofon, aktive Hintergrunddownloads, Datensynchronisierung, Vordergrunddienste oder Dienste, die durch einen Vordergrundclient mit Flags wie BIND_NOT_FOREGROUND gebunden sind (siehe Vererbung mit BIND-Flags steuern). Auch wenn einige dieser Arbeitslasten in internen Plattformmesswerten als PROCESS_STATE_IMPORTANT_FOREGROUND gekennzeichnet sind, bedeutet diese interne Konstante nicht, dass ein sichtbares Zeitfenster vorhanden ist. Sie unterliegt dem perceptible-Budget.
  • background: Aufgaben, die für den Nutzer nicht sofort wahrnehmbar sind, z. B. Hintergrundjobs, Wecker, Broadcast-Empfänger oder die vorherige Aktivität, nachdem der Nutzer die App verlassen hat.

In der folgenden Tabelle sehen Sie, wie die Wichtigkeitsstufen zur Laufzeit den Manifeststatus zugeordnet werden:

Manifest android:state Wichtigkeit zur Laufzeit (RunningAppProcessInfo) Typische Komponenten
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Fortgesetzte sichtbare Aktivität, Top-App bei gesperrtem Bildschirm, Dienst oder Contentanbieter, der an die Top-App gebunden ist
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Aktive Vordergrunddienste für die Medienwiedergabe, Navigation, Downloads oder Synchronisierung; Dienst, der an einen Vordergrunddienst gebunden ist oder sichtbare Flags verwendet
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
Hintergrundaufgaben, Empfänger, Hintergrundsynchronisierung, vorherige Aktivität oder Home App im Hintergrund

Weitere Informationen dazu, wie Clientbindungen einen Dienst- oder Anbieter-Hostprozess erhöhen und wie der Prozess nach dem Aufheben der Clientbindung oder dem Verschieben des Clients auf einen anderen Bildschirm wieder in den Zustand background oder cached übergeht, finden Sie unter Dienstbindungen und Prozessstatus.

Wenn ein Prozess in einen Cache-Zustand (IMPORTANCE_CACHED) ohne aktive Komponenten eintritt, gibt die Plattform das aktive Speicherbudget frei und verwaltet den Speicher durch die standardmäßige Rückgewinnung von Cache-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 abgekürzte Beispiel zeigt den Prozessdatensatz 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=4 und state: cur=FGS geben an, dass sich der Prozess im Status des Dienstes im Vordergrund befindet. Wenn in Ihrer App eine aktive sichtbare Aktivität gehostet wird, wird sie als TOP angezeigt. Wenn eine wichtige Vordergrundkomponente ausgeführt wird (z. B. ein Download oder eine Synchronisierung), wird das Symbol IMPF angezeigt.
  • In der Tabelle „Process OOM control“ (OOM-Steuerung für Prozesse) gibt prcp an, dass der Prozess in der Prioritätsstufe „Wahrnehmbar“ ausgewertet wird, die dem perceptible-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 (Arbeitsspeicherbudget), in dem die aktive Obergrenze, die Quelle des Limits (z. B. AndroidManifest oder MemoryBudgetManager) und der aktuelle resident-Speicher angezeigt werden.

Apps mit mehreren Prozessen

Wenn Ihre Anwendung die Arbeit auf mehrere Prozesse verteilt, konfigurieren Sie mit dem <process>-Tag in <processes> dedizierte Prozessbudgets.

Hier ein Beispiel für eine Musikstreaming-App (com.example.radio):

  1. Hauptprozess: Hier werden die sichtbare Benutzeroberfläche und die Audio-Wiedergabe-Engine gehostet (MediaSessionService mit einem mediaPlayback-Dienst im Vordergrund). Wenn sichtbar, wird der Prozess mit dem Vordergrundbudget von 180 MB ausgeführt. Wenn der Nutzer die App verlässt, während die Musik weiter abgespielt wird, wechselt der Prozess in den Status perceptible. In diesem Status reichen 64 MB für die Wiedergabe-Engine und den Audio-Puffer aus.
  2. Synchronisierungsprozess (:sync): Ein dedizierter Prozess, der die Synchronisierung von Hintergrundmetadaten und die Downloadindexierung ausführt. Da dieser Prozess nur im Hintergrund aktiv ist, müssen Sie state="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>
            <!-- 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>

Wenn Sie in Ihrem Manifest einen <processes>-Block deklarieren, fügen Sie für jeden Prozess, der von Komponenten in Ihrem zusammengeführten Manifest verwendet wird, einen <process>-Eintrag ein, einschließlich <process /> für den Standardanwendungsprozess.

Die Arbeitsspeichernutzung in einem beliebigen untergeordneten Prozess wird sowohl auf das Prozessbudget als auch auf das Budget des einschließenden Pakets angerechnet. Ein Prozess hat zur Laufzeit Arbeitsspeicherdruck, wenn er entweder sein Prozessbudget oder das Paketbudget überschreitet, je nachdem, welcher Grenzwert zuerst erreicht wird.

Budgets für Displays mit hoher Pixeldichte skalieren

Für Anwendungen, deren Speicherbedarf sich erheblich danach richtet, wie viele Pixel gleichzeitig auf dem Display gerendert werden müssen – z. B. eine Fotogalerie-App, die Bitmaps in Bildschirmgröße im Cache speichert – bietet Android zwei alternative Mechanismen, um Budgets dynamisch an die Displayspezifikationen anzupassen:

  • Nach Bucket für die Displaydichte skalieren (android:additionalMbPerDensity): Fügt Megabyte proportional zum Dichterverhältnis des Displays im Verhältnis zu mdpi (1,0x / 160 dpi) hinzu. Dies ist geeignet, wenn die Speichernutzung mit UI-Dichtebereichen skaliert wird, z. B. beim Caching von Raster-Drawables oder UI-Assets mit höherer Auflösung:

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

    Bei einem mdpi-Display (1,0‑fach) beträgt das Budget 180 + 16 × 1 = 196 MB. Auf einem xxhdpi-Display (3,0x) wird das Budget auf 180 + 16 × 3 = 228 MB skaliert.

  • Nach physischer Displayauflösung skalieren (android:additionalBytesPerDisplayPixel): Fügt Bytes direkt pro physischem Displaypixel (Breite × Höhe) hinzu. Das ist ideal für Anwendungen, die Grafikoberflächen im Vollbildmodus, Renderpuffer oder Fotocaches in voller Auflösung zuweisen, bei denen der Speicherverbrauch direkt mit der Anzahl der Rohanzeigepixel und nicht mit den UI-Dichtebereichen skaliert wird:

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

    Auf einem 1080p-Display (1080 × 2400 ≈ 2,59 Mio. Pixel) werden dem Basisbudget so etwa 41,4 MB hinzugefügt. Auf einem 1440p-Display (1440 × 3120 Pixel, ca. 4,49 Mio. Pixel) werden ca. 71,8 MB hinzugefügt.

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.

Für Geräteformfaktoren optimieren

Wenn Sie ein APK für Smartphones, Tablets und Wear OS bereitstellen, verwenden Sie das Attribut android:feature, um Budgets für verschiedene Hardwareziele anzupassen.

Auf Wear OS-Smartwatches ist der RAM begrenzt und die Benutzeroberfläche und das Feature-Set der App sind viel einfacher. Sie können ein engeres Budget speziell für die Funktion 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" />

Regel zur Beilegung: Die letzte anwendbare Klausel tritt in Kraft

Wenn Sie mehrere <memory-budget>-Elemente für eine Anwendung oder einen Prozess definieren, werden sie in der Reihenfolge ausgewertet, in der sie im Manifest deklariert sind. Die zuletzt anwendbare Budgetklausel wird durchgesetzt.

Da das letzte anwendbare Budget Vorrang hat, ist die Reihenfolge wichtig. Das allgemeinste Baseline-Budget sollte immer zuerst angegeben werden, gefolgt von spezifischeren Überschreibungen (z. B. klauseln für bestimmte Bundesstaaten oder Hardware).

Referenz zu XML-Attributen

Alle Attribute für die Arbeitsspeichergröße werden in Megabyte (MB) angegeben und entsprechen der Linux-Cgroup-Gebühr memory.current, die den gemeinsamen Arbeitsspeicher wie den Zygote-Prozess ausschließt.

Attribut Format Standard Beschreibung
android:maxMb Ganzzahl (> 0) Erforderlich Das Baseline-Limit für das Resident-Memory-Budget in MB.
android:state Enum Beliebig Der Prozessstatus, für den dieses Budget gilt: foreground, perceptible oder background. Wird sie weggelassen, dient sie als Fallback für alle nicht angegebenen Status.
android:additionalMbPerDensity Ganzzahl (≥ 0) 0 Zusätzliche Megabyte, die pro Einheit des Verhältnisses der Displaydichte relativ zu mdpi (1,0x) hinzugefügt werden sollen.
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 die bevorzugte Lösung für fast alle Apps. Für Anwendungen mit dynamischen Arbeitslasten oder für Laufzeit-Experimente bietet Android jedoch Laufzeit-SDK- und NDK-APIs als sekundäre Option.

Mit der Laufzeit-API haben Sie folgende Möglichkeiten:

  • Aktuelle Arbeitsspeichernutzung und effektive Budgets abfragen
  • Prozessbudget dynamisch nach unten anpassen
  • Achten Sie auf Ereignisse, die das Budget überschreiten, um Caches proaktiv zu verkleinern, bevor das Betriebssystem die direkte Rückforderung auslöst.

Android SDK API (MemoryBudgetManager)

Der Systemdienst MemoryBudgetManager ist für Apps verfügbar, die in Kotlin und Java geschrieben wurden und auf Android 17 QPR2 (untergeordnetes SDK-Release, API-Level 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2) basieren.

Dienst abrufen

Bevor Sie auf MemoryBudgetManager zugreifen, prüfen Sie mit SDK_INT_FULL, ob auf dem Gerät eine niedrigere Version als Android 17 QPR2 ausgeführt wird:

if (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 geringeres Budget festlegen, um den Arbeitsspeicher bei einfachen Aufgaben zu begrenzen, oder es nach Abschluss der Aufgabe löschen:

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

Anders als bei <memory-budget>-Manifestklauseln, bei denen Budgets automatisch gewechselt werden, wenn der Prozess zwischen den Status foreground, perceptible und background wechselt, wird bei einem dynamischen Budget, das über MemoryBudgetManager festgelegt wird, eine einzelne Obergrenze angewendet, bis Ihr Code sie aktualisiert oder löscht. Wenn Sie die Laufzeit-API verwenden, um Feldtests durchzuführen oder zustandsspezifische Limits zu verwalten, müssen Sie das dynamische Budget bei Übergängen im Lebenszyklus aktualisieren oder löschen (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 Budget-Mutations-APIs sind idempotent. Das wiederholte Aufrufen von clearProcessBudget() oder clearPackageBudget() oder das Aufrufen, wenn kein dynamisches Budget aktiv ist, ist ein sicherer No-Op. Wenn Sie setProcessBudgetBytes() oder setPackageBudgetBytes() nacheinander auf denselben Wert setzen, werden keine redundanten Systemrekonfigurationen ausgelöst.
  • Aufrufhäufigkeit: Wenn Sie ein Budget festlegen, aktualisieren oder löschen, wird ein prozessübergreifender Aufruf an den Systemdienst gesendet, um den Arbeitsspeicher-Controller des Betriebssystems für den Prozess zu aktualisieren. Sie ist zwar leicht, aber nicht völlig kostenlos. Rufen Sie diese APIs bei diskreten Lebenszyklusübergängen und Aufgabenmeilensteinen auf, z. B. beim Starten oder Beenden einer Aktivität, beim Starten oder Beenden der Hintergrundverarbeitung oder beim Verarbeiten von Sprachanfragen. Vermeiden Sie es, sie in engen Schleifen, Audioverarbeitungs-Threads oder Per-Frame-Rendering-Routinen aufzurufen.
  • Abfragehäufigkeit: Mit den Abfrageattributen processCurrentUsageBytes und packageCurrentUsageBytes wird die aktuelle Arbeitsspeichernutzung vom Systemdienst abgerufen. Abfragen sollten zwar schnell sein, aber nur bei Bedarf aufgerufen werden (z. B. bei Aufgabenübergängen oder in OnOverBudgetListener-Callbacks) und nicht in kontinuierlichen Schleifen abgefragt werden.

Auf Anrufe mit dem Hinweis auf Budgetüberschreitung reagieren

Apps können einen Listener registrieren, um benachrichtigt zu werden, wenn die Arbeitsspeichernutzung den Budgetschwellenwert überschreitet. So kann die App proaktiv Bereinigungen auf Anwendungsebene durchführen, z. B. das Leeren von Bitmap-Caches im Arbeitsspeicher, bevor das Betriebssystem die Latenz für die direkte Rückforderung auslöst:

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict caches to release memory
    imageTileCache.evictAll()
}

// Register on the main Looper
budgetManager.registerProcessOverBudgetListener(mainLooper, listener)

// When done (e.g., in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

Best Practices für Rückrufe bei Überschreitung des Budgets:

  • Schnell reagieren: Bei Rückforderungsmaßnahmen muss sofort Abhilfe geschaffen werden. Komplexe Berechnungen unter Belastung verschlechtern die Leistung.
  • Zuweisungen vermeiden: Weisen Sie im Callback keine neuen Objekte zu und starten Sie keine neuen Threads, da dies zu einer sofortigen direkten Rückforderung durch das Betriebssystem führen kann.
  • Auf Objekte mit hoher Ausbeute konzentrieren: Das Entfernen großer Bitmaps, Renderpuffer oder das Schließen von Memory-Mapped-Dateien ist viel effektiver als das Freigeben vieler kleiner Objekte.

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

Native Apps können die C-NDK-API verwenden, die ab Android 17 QPR2 (API-Level 37.2) von libandroid.so bereitgestellt wird.

CMake-Konfiguration

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

Header- und Abfragenutzung einschließen

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

Arbeitsspeicherdruckereignisse überwachen

Das NDK bietet zwei Möglichkeiten, Speicherereignisse zu beobachten:

  1. High-Level Watcher (AMemoryBudgetManager_Watcher_create): Überwacht Ereignisse in einem ALooper mit automatischer Entprellung.
  2. Dateideskriptor auf niedriger Ebene: AMemoryBudgetManager_getProcessMemoryPressureFd gibt einen nativen Dateideskriptor zurück, der direkt in eine benutzerdefinierte epoll-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 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);

Beispiele für Runtime API

Die folgenden Beispiele zeigen, wie Sie die Laufzeit-APIs mit der Android SDK API (in Kotlin geschrieben) und der Native NDK API (in C++ geschrieben) implementieren.

Android SDK-Beispiel: Adaptiver Bildeditor

In diesem Beispiel wird eine Bildbearbeitungs-App (com.example.imageeditor) gezeigt, in deren Manifest ein Limit von 256 MB für den mehrschichtigen Canvas deklariert wird:

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

Wenn der Nutzer die Miniaturansicht-Galerie durchsucht, verwendet die App die Android SDK API in Kotlin, um das Prozessbudget dynamisch auf 96 MB zu reduzieren. Wenn der Nutzer den mehrschichtigen Bearbeitungsbereich öffnet, löscht die App das dynamische Budget, um das vollständige Manifestlimit von 256 MB wiederherzustellen. Außerdem wird ein OnOverBudgetListener registriert, um unter Druck zwischengespeicherte Vorschau-Bitmaps zu entfernen.

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

NDK C++-Beispiel: Native 3D-Engine

In diesem Beispiel wird gezeigt, wie eine native C++-Game-Engine Speicherbudgets basierend auf dem aktiven Grafikqualitätsniveau verwaltet. Dabei wird davon ausgegangen, dass im Manifest der Anwendung eine Obergrenze von 512 MB für Grafiken in hoher Qualität (android:maxMb="512") festgelegt ist. Die Engine schränkt das Budget für Voreinstellungen mit niedrigerer Qualität dynamisch ein und verwendet AMemoryBudgetManager_Watcher_create für ein ALooper, um Texture-Mipmaps zu entladen, wenn das Budget überschritten wird.

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