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 die erwartete Menge an Arbeitsspeicher, die die App benötigt, und möchte sicherstellen, dass sie nicht zu viele der gemeinsamen RAM-Ressourcen des Systems verwendet.
Das Budget wird durch Speicherbereinigung und Swap 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, richtet das Betriebssystem die Rückforderung speziell auf diese App aus:
- Bereinigte seitenbasierte Dateien (z. B. inaktiver Code und zugeordnete Assets) werden zuerst entfernt, da sie bei Bedarf aus dem Speicher neu gelesen werden können.
- Dirty file-backed pages (geänderte Seiten, die auf Dateien basieren) werden in den Speicher zurückgeschrieben und entfernt.
- 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 gibt nicht verwendeten Arbeitsspeicher frei und komprimiert inaktive Heap-Seiten, um sie auszulagern, damit die Arbeitsspeicherzuweisungen begrenzt bleiben, ohne den Prozess zu beenden.
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 nach dem Start des Prozesses wirksam und es wird ein klarer Vertrag für das Betriebssystem bereitgestellt.
Ausgangsbudget deklarieren
Bei den 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 Speicher 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 reduziert.
Budgets nach Prozessstatus variieren
Eine App benötigt je nach Sichtbarkeit für Nutzer unterschiedliche Mengen an Arbeitsspeicher:
- 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, 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 zuzuordnen:
<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>
Sie müssen android:state="foreground" nicht in der ersten Klausel angeben. Eine Klausel ohne android:state dient als Standard-Fallback für alle Bundesstaaten. Die restriktiveren Klauseln darunter überschreiben das Budget, wenn die App in den Status perceptible oder background wechselt.
Apps mit mehreren Prozessen
Wenn Ihre Anwendung die Arbeit auf mehrere Prozesse verteilt, konfigurieren Sie mit dem Tag <process> in <processes> dedizierte Prozessbudgets.
Hier ein Beispiel für eine Musikstreaming-App (com.example.radio):
- Hauptprozess: Hier werden die sichtbare Benutzeroberfläche und die Audio-Wiedergabe-Engine gehostet (
MediaSessionServicemit einemmediaPlayback-Dienst im Vordergrund). Wenn der Prozess sichtbar ist, wird er mit dem Budget von 180 MB für den Vordergrund ausgeführt. Wenn der Nutzer die App verlässt, während die Musik weiter abgespielt wird, wechselt der Prozess in den Statusperceptible. In diesem Status reichen 64 MB für die Wiedergabe-Engine und den Audio-Puffer aus. - 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 Siestate="background"nicht explizit deklarieren. Es gilt ein einzelnes Budget.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.radio">
<application
android:label="@string/app_name">
<!-- Package baseline: main process with UI and audio playback -->
<memory-budget android:maxMb="180" />
<!-- Tighter budget when audio plays in the background -->
<memory-budget
android:maxMb="64"
android:state="perceptible" />
<!-- 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 zur 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, in der Bitmaps in Bildschirmgröße zwischengespeichert werden – 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 zumdpi(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 einemxxhdpi-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 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 für die Lösung: Die letzte anwendbare Klausel tritt in Kraft
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 wird durchgesetzt.
Da das letzte anwendbare Budget Vorrang hat, ist die Reihenfolge wichtig. Stellen Sie das allgemeine Baseline-Budget immer an den Anfang, gefolgt von spezifischeren Überschreibungen (z. B. klauseln für bestimmte Bundesstaaten oder Hardware).
Referenz zu XML-Attributen
Alle Attribute für die Speichergröße werden in Megabyte (MB) angegeben und entsprechen der Linux-Cgroup-Gebühr memory.current, die den gemeinsam genutzten Speicher wie den Zygote-Prozess ausschließt.
| Attribut | Format | Standard | Beschreibung |
|---|---|---|---|
android:maxMb |
Ganzzahl (> 0) | Erforderlich | Das residenter Speicher-Budgetlimit in MB. |
android:state |
Enum | Beliebig | Der Prozessstatus, für den dieses Budget gilt: foreground, perceptible oder background. |
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. |
Runtime 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-Tests bietet Android jedoch Laufzeit-SDK- und NDK-APIs als sekundäre Option.
Mit der Laufzeit-API haben Sie folgende Möglichkeiten:
- Aktuelle Speichernutzung 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.
Kotlin API (MemoryBudgetManager)
Der Systemdienst MemoryBudgetManager ist ab Android 17 QPR2 (Android 26Q4 SDK-Release, API-Level 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2) verfügbar.
Dienst abrufen
val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
Verwendung von Abfragen 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-declared ceiling
Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}
// Clear the dynamic process budget to restore the manifest limit
budgetManager.clearProcessBudget()
Auf Anrufe zur 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: Die Rückforderung muss sofortige Abhilfe schaffen. Komplexe Berechnungen unter Druck 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 speicherabgebildeten 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 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
AMemoryBudgetManager_clearProcessBudget();
Arbeitsspeicherdruck-Ereignisse beobachten
Das NDK bietet zwei Möglichkeiten zum Überwachen von Speicherereignissen:
- High-Level Watcher (
AMemoryBudgetManager_Watcher_create): Überwacht Ereignisse in einemALoopermit automatischer Entprellung. - Low-Level-Dateideskriptor: Mit
AMemoryBudgetManager_getProcessMemoryPressureFdwird ein nativer Dateideskriptor zurückgegeben, der direkt in eine benutzerdefinierteepoll-Engine-Schleife eingebunden werden kann.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
// High-yield eviction of unused native textures or geometry caches
purgeNativeTextureCaches();
}
// Register watcher on 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 die Runtime API
Die folgenden Beispiele zeigen, wie die Laufzeit-APIs in Kotlin und C++ implementiert werden.
Kotlin-Beispiel: Adaptiver Bildeditor
In diesem Beispiel wird eine Bildbearbeitungs-App (com.example.imageeditor) gezeigt, die ihr Arbeitsspeicherbudget dynamisch erhöht, wenn der Nutzer einen mehrschichtigen Bearbeitungsbereich öffnet, und das dynamische Budget löscht, wenn er zur Miniaturgalerie zurückkehrt. 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)
}
override fun onStop() {
super.onStop()
budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
}
/**
* Called when the user enters the high-resolution editing canvas.
*/
fun enterEditingCanvas() {
try {
// Dynamically set budget to 256MB for the editing canvas
budgetManager.processBudgetBytes = 256L * 1024L * 1024L
Log.i(TAG, "Dynamic budget applied: 256MB")
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Could not apply dynamic budget", e)
}
}
/**
* Called when the user exits the editor back to the thumbnail gallery.
*/
fun exitToGallery() {
previewCache.trimToSize(8 * 1024 * 1024)
// Clear dynamic budget; restores the baseline manifest budget
budgetManager.clearProcessBudget()
}
companion object {
private const val TAG = "ImageEditor"
}
}
NDK C++-Beispiel: Native 3D-Engine
In diesem Beispiel wird gezeigt, wie eine native C++-Spiele-Engine Speicherbudgets basierend auf dem aktiven Grafikqualitätsniveau verwaltet. Dazu wird AMemoryBudgetManager_Watcher_create auf einem ALooper verwendet, um Textur-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;
};