App-Arbeitsspeicherbudgets festlegen

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:

  1. 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.
  2. Dirty-Seiten, die durch Dateien gesichert sind, werden in den Speicher zurückgeschrieben und entfernt.
  3. 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:

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.

Eine detaillierte Anleitung finden Sie unter Arbeitssatz messen und Budgets kalibrieren.

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 wie BIND_NOT_FOREGROUND gebunden sind (siehe Vererbung mit BIND-Flags steuern). In internen Plattformmesswerten werden einige dieser Arbeitslasten möglicherweise als PROCESS_STATE_IMPORTANT_FOREGROUND bezeichnet. Diese interne Konstante bedeutet jedoch kein sichtbares Fenster und wird durch das perceptible-Budget geregelt.
  • background: Arbeit, die für den Nutzer nicht sofort erkennbar ist, z. B. Hintergrundjobs, Alarme und Übertragungsempfänger.

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

Manifest android:state Laufzeitbedeutung (RunningAppProcessInfo) Typische Komponenten
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Fortgesetzte sichtbare Aktivität, oberste App bei gesperrtem Bildschirm, Dienst oder Contentanbieter, der an die oberste App gebunden ist
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Aktive Medienwiedergabe, Navigation, Downloads oder Synchronisierung von Vordergrunddiensten; Dienst mit Vordergrunddienst oder sichtbaren Flags gebunden
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
Hintergrundjobs, Empfänger, Hintergrundsynchronisierung

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=4 und state: cur=FGS geben an, dass sich der Prozess im Status „Dienst im Vordergrund“ befindet. Wenn in Ihrer App eine aktive sichtbare Aktivität gehostet wird, wird dies als TOP angezeigt. Wenn eine wichtige Vordergrundkomponente ausgeführt wird (z. B. ein Download oder eine Synchronisierung), wird IMPF angezeigt.
  • In der OOM-Steuerungstabelle für Prozesse gibt prcp an, dass der Prozess unter der wahrnehmbaren Prioritätsstufe 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, 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):

  1. Hauptprozess: Hostet die sichtbare Benutzeroberfläche und die Audiowiedergabe-Engine (MediaSessionService mit einem mediaPlayback-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 Status perceptible. Hier reicht ein Budget von 64 MB für die Wiedergabe-Engine und den Audiopuffer aus.
  2. Synchronisierungsprozess (:sync): Dedizierter Prozess, der die Metadatensynchronisierung im Hintergrund 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>
            <!-- 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 zu mdpi (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 einem xxhdpi-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 &approx; 2,59 Mio. Pixel) werden dem Basisbudget &approx; 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.

Arbeits-Set messen und Budgets kalibrieren

Ein gutes Budget liegt knapp über dem Working Set deiner App für einen bestimmten Status, nicht bei der unbegrenzten Spitze. Um diesen Punkt zu finden, führen Sie einen realistischen User-Flow aus, senken Sie das Budget schrittweise und achten Sie darauf, wann Ihre Anwendung dafür bezahlt.

Unterschied zwischen Arbeitssatz und residentem Speicher

Ohne ein Budget enthält der Resident Footprint (memory.current) Ihrer App zwei Arten von Arbeitsspeicher:

  • Arbeitssatz: der Java-, Kotlin- und native Heap sowie die häufig verwendeten dateigestützten Seiten (DEX-Code, native Bibliotheken und Assets), die im aktuellen User-Flow immer wieder aufgerufen werden.
  • Cold Slack: Bitmap-Caches von früheren Bildschirmen, einmalige Startpuffer, Heap-Slack, der nach einem Zuordnungsburst übrig bleibt, und Dateiseiten, die einmal gelesen und nie wieder berührt wurden

Ein Budget auf dem unbeschränkten Höchstwert fordert die ungenutzte Kapazität nie zurück. Wenn das Budget unter dem Arbeitssatz liegt, lagert der Kernel Heap-Seiten aus und entfernt Codeseiten, die Ihre App sofort wieder einliest. Diese Working-Set-Refaults kosten CPU-Zeit und Akku. Sobald sie im UI-Thread landen, kosten sie auch Frames.

Sie suchen nach dem Knick: dem niedrigsten Budget, bei dem durch die Rückforderung nur kalter Slack entfernt wird, sodass die Anzahl der Refaults, die CPU-Zeit und die Frame Times so bleiben, wie sie ohne Budget waren.

Testumgebung vorbereiten

Richten Sie einen wiederholbaren Test ein, der sich wie die Produktion verhält:

  • Kalibrieren Sie denselben R8-optimierten, nicht debuggbaren Release-Build, den Sie auch veröffentlichen. Debug-Builds verbrauchen mehr Arbeitsspeicher. Ein Budget, das auf einen Debug-Build abgestimmt ist, ist daher für den Release zu hoch.
  • Testen Sie die App auf einem Gerät oder Emulator mit Android 17 QPR2 (API-Level 37.2) oder höher. Ein userdebug-Build oder ein Emulator-Image ohne Google Play ist am besten, da Sie auf einigen Geräten die cgroup-Dateien in den folgenden Abschnitten erst lesen können, nachdem Sie adb root ausgeführt haben.
  • Erstellen Sie mit Jetpack Macrobenchmark, UI Automator oder adb shell input ein Skript für einen 30- bis 60-sekündigen Nutzerfluss, und zwar einmal für jeden Zustand, für den Sie ein Budget einplanen möchten. Eine inaktive Anwendung sagt nichts über ihren Arbeitssatz aus.
    • foreground: Scrollen Sie durch dichte Listen und Medienraster, wechseln Sie zwischen Ihren Hauptzielen, öffnen Sie Menüs und Dialogfelder und führen Sie Animationen aus.
    • perceptible: Verlassen Sie das sichtbare Fenster, während die wahrnehmbare Arbeit fortgesetzt wird (z. B. Audiowiedergabe oder Download), lassen Sie sie 10 bis 15 Sekunden lang laufen und kehren Sie dann zur App zurück.
    • background: Synchronisierung, WorkManager-Job (z. B. mit adb shell cmd jobscheduler run -f <package-name> <job-id>), gestarteter Dienst oder Übertragungsempfänger wird ausgelöst, während die App keine sichtbare Benutzeroberfläche hat

Messen Sie ein background-Budget, während die Hintergrundarbeit ausgeführt wird. Sobald der Prozess im Cache gespeichert wird, gibt die Plattform sein Budget frei (siehe Zuordnung von Budgetstatus zu Prozessstatus). Ein inaktiver im Cache gespeicherter Prozess sagt also nichts über Ihr background-Budget aus.

Normalbereich ermitteln

Beginnen Sie mit Ausführungen, die überhaupt kein Budget haben, damit Sie wissen, wie ein normaler Ablauf aussieht. Verwenden Sie einen Build ohne <memory-budget>-Deklarationen und starten Sie jeden Durchlauf mit einem neuen Prozess (adb shell am force-stop <package-name>).

Jeder Anwendungsprozess hat einen eigenen cgroup v2-Speichercontroller. Suche den Pfad in der Zeile 0:: von /proc/<PID>/cgroup, anstatt ihn selbst zu erstellen, da sich das cgroup-Layout von Gerät zu Gerät unterscheiden kann. Übergeben Sie jeden Gerätebefehl als einzelnen String in Anführungszeichen an adb shell, damit die Geräte-Shell die Anführungszeichen sieht:

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"

Das Lesen dieser Dateien ist kostengünstig und beeinträchtigt Ihre Anwendung nicht. Sie können sie daher während einer Ausführung jede Sekunde abfragen:

  • memory.current: Residenter Arbeitsspeicher, der dem Prozess in Byte in Rechnung gestellt wird. Dies ist die Anzahl, die durch <memory-budget> begrenzt wird.
  • memory.swap.current: Arbeitsspeicher, der in den zRAM-Swap komprimiert wurde, in Byte
  • anon und file in memory.stat: residenter anonymer und dateibasierter Speicher in Byte
  • workingset_refault_anon und workingset_refault_file in memory.stat: Laufende Anzahl der ausgelagerten oder entfernten Seiten, die Ihre App wieder benötigt hat. Diese Zähler sind der wichtigste Hinweis darauf, dass ein Budget zu knapp ist.

Wenn Sie einen Berechtigungsfehler erhalten, führen Sie adb root aus und versuchen Sie es noch einmal. Wenn Sie keinen Root-Zugriff erhalten, bietet adb shell dumpsys meminfo <package-name> eine gröbere Ansicht. In App Summary sind Java Heap, Native Heap und Stack größtenteils anonymer Speicher und Code ist dateigestützt. Software-Bitmaps werden auf Native Heap und Ihr Budget angerechnet, Hardware-Bitmaps und Graphics dagegen nicht. Führe dumpsys meminfo nicht in einer Schleife aus, während du das Frame-Timing misst. Dadurch wird eine automatische Speicherbereinigung in Ihrer App erzwungen und alle Speicherzuordnungen werden durchlaufen. Das ändert, was Sie messen möchten.

Kandidatenbudgets bereinigen

Sie können Kandidatenbudgets ausprobieren, ohne Ihre Anwendung neu zu erstellen. Verwenden Sie am memory-limiter manual, um ein Budget für einen laufenden Prozess festzulegen, und none, um es zu entfernen:

adb shell am memory-limiter manual <PID> 120MB
adb shell am memory-limiter manual <PID> none

Eine manuelle Überschreibung gilt für die gesamte Lebensdauer dieser Prozessinstanz, auch bei Änderungen des Prozessstatus. Löschen Sie sie daher oder starten Sie den Prozess neu, bevor Sie fortfahren. Außerdem wird das harte Limit (memory.max) des Prozesses auf denselben Wert festgelegt. Wenn das Budget unter dem Arbeitssatz Ihrer App liegt, kann der Prozess beendet werden, anstatt nur verlangsamt zu werden. Behandeln Sie dies als fehlgeschlagenes Budget.

Sie können auch einen Benchmark-Build durchsuchen, indem Sie einen Kandidatenwert aus einem Instrumentierungsargument lesen und MemoryBudgetManager.processBudgetBytes festlegen. Ein Laufzeitbudget kann ein Manifestbudget nur einschränken. Führen Sie daher einen Sweep für einen Build ohne <memory-budget>-Deklarationen aus. Sie bleibt auch bei Änderungen des Prozessstatus in Kraft, bis Sie clearProcessBudget aufrufen.

Beginnen Sie mit dem höchsten memory.current-Wert aus Ihrer Baseline und verringern Sie ihn jeweils um 10 bis 15 MB. Für jedes Budget, das infrage kommt, gilt:

  1. Starten Sie einen neuen Prozess, bringen Sie ihn in den Zielzustand und wenden Sie das Budget an.
  2. Führen Sie den User-Flow einmal zum Aufwärmen aus. Wenn Sie ein Budget anwenden, werden Kaltstartseiten einmalig entfernt. Sie möchten jedoch den stabilen Zustand und nicht diesen Übergang messen.
  3. Setzen Sie die Frame-Statistiken zurück und zeichnen Sie die Startzähler für Refaults und die CPU-Zeit des Prozesses (utime und stime, in Taktzyklen) auf:

    adb shell dumpsys gfxinfo <package-name> reset
    adb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"
    adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"
  4. Führen Sie den gemessenen Durchlauf des User-Flows aus.

  5. Zeichnen Sie memory.current, anon, file, die Refault-Zähler und die CPU-Zeit noch einmal auf und erfassen Sie Frame-Statistiken. dumpsys gfxinfo kann dieselben Statistiken mehrmals ausgeben. Nehmen Sie daher die erste Übereinstimmung, die die gesamte App abdeckt:

    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'
    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'

Wiederholen Sie jeden Kandidaten mindestens dreimal und verschachteln Sie die Kandidaten, anstatt alle Wiederholungen eines Kandidaten nacheinander auszuführen. Vergleichen Sie jedes Ergebnis mit der Streuung Ihrer Referenzläufe, nicht mit einem einzelnen Lauf.

Ergebnisse lesen

Wenn das Budget sinkt, durchläuft Ihre App drei Phasen:

  • Über dem Knie: memory.current folgt dem Budget nach unten, indem kalte file-Seiten und inaktiver Heap entfernt werden. anon bleibt flach, die Anzahl der Refaults bleibt nahe der Baseline und die CPU-Zeit und die Frame Times ändern sich nicht. Hier möchten Sie sein.
  • Unterhalb des Knies: Die kalten Seiten sind weg, sodass der Kernel beginnt, Heap-Seiten auszulagern, die Ihre App noch verwendet. Die Anzahl der Refaults steigt pro Ausführung um Hunderte oder Tausende von Seiten und die CPU-Zeit erhöht sich. Die Frame Times ändern sich auf einem ansonsten inaktiven Testgerät möglicherweise kaum, aber Ihre App verbraucht jetzt CPU-Zeit und Akkuleistung, um ihren eigenen Arbeitssatz zu komprimieren und zu dekomprimieren.
  • Weit unter dem Knie: Reclaim- und Swap-In-Fehler landen im UI-Thread und in RenderThread. Ruckler, Frame Times und Warm-Resume-Zeiten steigen an.

Im Vergleich zur uneingeschränkten Baseline wird ein Kandidatenbudget akzeptiert, wenn es alle folgenden Bedingungen erfüllt, die einen angemessenen Ausgangspunkt darstellen:

  • Der Prozess überlebt die Ausführung
  • Refault-Deltas bleiben nahe am Referenzwert
  • Die CPU-Zeit des Prozesses steigt um nicht mehr als 5 %.
  • Falsch gerenderte Frames steigen um maximal 1 Prozentpunkt
  • P95-Frame-Time steigt um maximal 3 ms
  • Die Warm-Resume-Zeit (TotalTime von adb shell am start -W) steigt um nicht mehr als 15 % für perceptible- und background-Budgets

Der Knickpunkt ist das niedrigste Kandidatenbudget, das die Prüfung besteht. Hier ist ein Beispiel für einen Vordergrund-Sweep für eine hypothetische App mit einer unbeschränkten Spitze von 138 MB:

Budget des Kandidaten memory.current anon file Refaults (Seiten) P95-Frame-Time Regime
Keine 138 MB 75 MB 63 MB 0 10,5 ms Referenz
125 MB 121 MB 74 MB 47 MB 10 10,5 ms Kurz
110 MB 110 MB 75 MB 35 MB 15 10,8 ms Knie
95 MB 95 MB 74 MB 21 MB 176 11,2 ms Kniebedeckend
85 MB 85 MB 68 MB 17 MB 2.050 12,8 ms Kniebedeckend
70 MB 70 MB 54 MB 16 MB 8.771 16,5 ms Deutlich unter dem Knie

Budget auswählen

Legen Sie android:maxMb 10 % bis 20 % über dem Schwellenwert fest, damit Ihre App nicht aufgrund von Unterschieden bei Nutzerdaten, Sprachen und Zuweisungsspitzen darunter fällt. Im vorherigen Beispiel liegt der Knick bei 110 MB, daher sind 125 MB eine gute Wahl.

Installieren Sie dann einen Release-Build mit den neuen <memory-budget>-Deklarationen, führen Sie den User-Flow noch einmal aus und prüfen Sie, ob die Kriterien für das Bestehen weiterhin erfüllt sind. dumpsys meminfo zeigt das aktive Budget und seine Quelle im Abschnitt „Memory Budget“ an.

Nicht im Vordergrund ausgeführte Budgets mit einer warmen Rückgabe validieren

Ein knappes perceptible- oder background-Budget kann in diesem Zustand in Ordnung sein, aber Kosten verursachen, wenn der Nutzer zurückkehrt. Testen Sie auch die Rückfahrt:

  1. Führen Sie den Vordergrundablauf aus, verschieben Sie die App in den Zielstatus und wenden Sie das Kandidatenbudget mit am memory-limiter manual an.
  2. Lassen Sie die wahrnehmbare oder Hintergrundarbeit 10 bis 15 Sekunden lang laufen.
  3. Löschen Sie die Überschreibung (adb shell am memory-limiter manual <PID> none), rufen Sie die App dann sofort mit adb shell am start -W wieder auf und zeichnen Sie TotalTime auf. Wenn Sie den Kandidaten stattdessen über MemoryBudgetManager festlegen, rufen Sie clearProcessBudget auf, bevor die App zurückkehrt.
  4. Führen Sie eine kurze Vordergrundinteraktion aus und prüfen Sie die Refaults und Frame-Statistiken anhand der Kriterien für das Bestehen.

Wähle das niedrigste Budget aus, das sowohl im aktuellen Zustand der App als auch bei der Rückkehr aus dem Hintergrund ausreicht, und füge dieselben 10 % bis 20 % hinzu.

Displayskalierung kalibrieren

Wenn du android:additionalBytesPerDisplayPixel verwendest, suche den Vordergrund-Knick auf zwei Displays mit unterschiedlichen Auflösungen, z. B. 1080 × 2400 (ca. 2,59 Mio. Pixel) und 1440 × 3120 (ca. 4,49 Mio. Pixel). Teilen Sie die Differenz zwischen den beiden Knien in Byte durch die Differenz der Pixelanzahl, um android:additionalBytesPerDisplayPixel zu erhalten. Subtrahiere diesen Pro-Pixel-Betrag für das kleinere Display vom Knick, um die auflösungsunabhängige Basis zu erhalten, und füge Headroom hinzu, um android:maxMb zu erhalten.

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() oder clearPackageBudget() oder wenn kein dynamisches Budget aktiv ist, ist ein sicherer No-Op. Ebenso löst das wiederholte Festlegen von setProcessBudgetBytes() oder setPackageBudgetBytes() 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 processCurrentUsageBytes und packageCurrentUsageBytes rufen die aktuelle Arbeitsspeichernutzung vom Systemdienst ab. Abfragen sind zwar schnell, sollten aber bei Bedarf aufgerufen werden (z. B. bei Aufgabenübergängen oder in OnOverBudgetListener-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-Looper verwenden: Übergeben Sie nach Möglichkeit einen Hintergrund-HandlerThread Looper an registerProcessOverBudgetListener oder registerPackageOverBudgetListener. 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 Sie Looper.getMainLooper fü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 (oder unregisterPackageOverBudgetListener) im Callback auf oder schützen Sie die Bereinigung mit einem Wartezeit-Zeitstempel oder AtomicBoolean.
  • Vor dem Verringern eines Budgets registrieren: Wenn die aktuelle Speichernutzung bereits ein neues Budget überschreitet, wenn Sie setProcessBudgetBytes oder setPackageBudgetBytes aufrufen, 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:

  1. High-Level Watcher (AMemoryBudgetManager_Watcher_create): Überwacht Ereignisse auf einem ALooper mit automatischer Entprellung.
  2. Low-Level-Dateideskriptor: 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 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;
};