App-Arbeitsspeicherbudgets festlegen

Mit App-Arbeitsspeicherbudgets können Apps ein Arbeitsspeicherbudget für sich selbst 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 auf Geräte mit wenig Arbeitsspeicher ausgerichtet sind. In diesen Fällen kennt der Entwickler den erwarteten Arbeitsspeicher-Working Set und möchte sicherstellen, dass seine App nicht zu viele der gemeinsamen RAM-Ressourcen des Systems verwendet.

Das Budget wird durch Entfernen und Auslagern von Arbeitsspeicher ausgeglichen, um Arbeitsspeicherseiten zu entfernen, die nicht kürzlich verwendet wurden. So wird der Arbeitsspeicherbedarf der App auf den aktuellen Arbeitsbereich 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 (geänderte Seiten, die auf Dateien basieren) werden zurück in den Speicher geschrieben und entfernt.
  3. Anonyme Speicherseiten (z. B. Heap-Zuweisungen) werden komprimiert und in zRAM ausgelagert.

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

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 detaillierte Routenführung 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 diesen Status zu entsprechen:

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

Zuordnung von Budgetstatus zu Prozessstatus

Die Plattform ordnet Manifestbudgetstatus basierend auf RunningAppProcessInfo.importance Laufzeitprozessstatus zu:

  • foreground: Aktive, sichtbare Nutzerinteraktionen, z. B. das Hosten einer fortgesetzten Aktivität oder das Beibehalten des Status der obersten App, wenn der Bildschirm ausgeschaltet 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 oder Datenabgleich-Dienste im Vordergrund. In internen Plattformmesswerten werden einige dieser Arbeitslasten möglicherweise mit PROCESS_STATE_IMPORTANT_FOREGROUND gekennzeichnet. Diese interne Konstante steht jedoch nicht für ein sichtbares Zeitfenster und unterliegt dem perceptible-Budget.
  • background: Aufgaben, die für den Nutzer nicht sofort wahrnehmbar sind, z. B. Hintergrundjobs, Wecker, Broadcast-Empfänger oder Prozesse im Cache.

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 Display
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Vordergrunddienste für aktive Medienwiedergabe, Navigation, Downloads oder Synchronisierung
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
IMPORTANCE_CACHED
Hintergrundjobs, Receiver, Hintergrundsynchronisierung, Prozesse im Cache

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 Arbeitsspeichernutzung:

adb shell dumpsys meminfo <package-name>

Ab Android 17 QPR2 enthält diese Ausgabe den Abschnitt Memory Budget (Speicherbudget), 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 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 der Prozess sichtbar ist, wird er 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 Fall 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 Indexierung von Downloads 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" />

        <!-- Dedicated background sync process -->
        <processes>
            <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>

Die Arbeitsspeichernutzung in einem beliebigen untergeordneten Prozess wird sowohl auf das Prozessbudget als auch auf das Budget des einschließenden Pakets angerechnet. Ein Prozess ist während der Laufzeit einem Speicherdruck ausgesetzt, 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 Kompaktheitsgrad skalieren (android:additionalMbPerDensity): Fügt Megabyte proportional zum Kompaktheitsgrad des Displays im Verhältnis zu mdpi (1,0x / 160 dpi) hinzu. Dies ist geeignet, wenn die Speichernutzung mit den 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 Grafiken 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 kleineres 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 berücksichtigt wird, 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 gemeinsam genutzten 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 Anzeigedichte 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 (untergeordnete SDK-Version, 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()

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 sofortige Abhilfe geschaffen werden. Komplexe Berechnungen während des Drucks verschlechtern die Leistung.
  • Zuweisungen vermeiden: Weisen Sie im Callback keine neuen Objekte zu und starten Sie keine neuen Threads, da dies dazu führen kann, dass das Betriebssystem sofort direkt zurückfordert.
  • 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 beobachten

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 eine Obergrenze 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 Manifestlimit von 256 MB wiederherzustellen. Außerdem wird ein OnOverBudgetListener registriert, um zwischengespeicherte Vorschaubits bei Bedarf 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;
};