Bitmap-Objekte sind oft die größten einzelnen Faktoren für den Speicherbedarf einer Anwendung. Ob App-Symbole, Benachrichtigungsbilder oder Medieninhalte – eine ineffiziente Bitmap-Verarbeitung kann schnell zu Out-Of-Memory-Fehlern (OOM) und systemweitem Speicherdruck führen.
Bitmap-Konfigurationen und Pixeldaten
Die Menge an Arbeitsspeicher, die ein Bitmap belegt, wird hauptsächlich durch seine Abmessungen (Breite × Höhe) und seine Konfiguration (Bitmap.Config) bestimmt.
Die Konfiguration definiert, wie viele Byte zur Darstellung jedes Pixels verwendet werden:
| Konfiguration | Bytes pro Pixel | Beschreibung |
|---|---|---|
ALPHA_8 |
1 | Nur Alphakanal (Transparenz). Nützlich für Masken. |
RGB_565 |
2 | Rot (5 Bit), Grün (6 Bit), Blau (5 Bit). Kein Alpha. Gut für blickdichte Bilder, bei denen eine hohe Farbtreue nicht entscheidend ist. |
ARGB_8888 |
4 | Alpha, Rot, Grün, Blau (jeweils 8 Bit). Standardeinstellung und am häufigsten verwendet. |
RGBA_F16 |
8 | Gleitkommazahl mit halber Genauigkeit. Wird für Inhalte mit großem Farbraum und HDR-Inhalte verwendet. |
HARDWARE |
– | Im Grafikspeicher gespeichert (gralloc/DMABuf). Weitere Informationen finden Sie unter Hardware-Bitmaps. |
Formel für den Arbeitsspeicher:Memory (Bytes) = Width × Height × Bytes Per Pixel
Ein Vollbild auf einem 1080p-Gerät (1920 × 1080) in ARGB_8888
benötigt beispielsweise: 1920 × 1080 × 4 Bytes ≈ 8, 3 MB.
Heap-Bitmaps im Vergleich zu freigegebenen Bitmaps
Heap-Bitmaps (nativer Heap)
In modernen Android-Versionen (8.0 und höher) werden Bitmap-Pixeldaten im nativen Heap gespeichert, während sich im Java-Heap nur ein kleines Wrapper-Objekt befindet.
Wenn eine App ein Bild präsentieren muss, wird es normalerweise aus einer komprimierten Bilddatei in ein Bitmap decodiert und im Heap gespeichert.
Gemeinsam genutzte Bitmaps (ashmem/memfd)
Wenn ein Bitmap zwischen Prozessen übertragen wird (z.B. über Binder an SystemUI für eine Benachrichtigung), vermeidet Android das Kopieren der Pixeldaten durch die Verwendung von gemeinsamem Arbeitsspeicher (ashmem oder memfd).
Eine Bitmap-Instanz kann explizit in den gemeinsam genutzten Speicher kopiert werden, indem Bitmap.asShared() aufgerufen wird. Sie kann auch implizit kopiert werden, wenn eine Bitmap in ein Parcel eingefügt wird (in der Regel durch Hinzufügen des Bitmaps zu einem Parcelable wie einem Bundle) und über Binder IPC gesendet wird.
Wenn eine gemeinsam genutzte Bitmap über Binder IPC gesendet wird, werden die Pixeldaten selbst nicht kopiert, sondern ein Dateideskriptor, der auf einen gemeinsam genutzten Speicherbereich verweist, wird für den Empfängerprozess dupliziert. Der zugrunde liegende Speicherbereich kann von mehreren Prozessen gemeinsam genutzt werden und wird erst freigegeben, wenn alle darauf verweisenden Dateideskriptoren geschlossen wurden.
Veränderliche und unveränderliche Bitmaps
- Veränderliche Bitmaps: Können nach der Erstellung geändert werden, z.B. über ein
Canvas. Sie erfordern immer eine eigene private Arbeitsspeicherzuweisung. Wenn eine veränderliche Bitmap kopiert wird, muss eine vollständige Kopie (zweite Kopie aller Pixeldaten) erstellt werden. - Unveränderliche Bitmaps: Können nicht geändert werden. Dies ermöglicht Optimierungen wie die gemeinsame Nutzung desselben zugrunde liegenden Speicherpuffers zwischen verschiedenen
Bitmap-Instanzen. Bitmaps, die aus APK-Ressourcen (BitmapFactory) geladen werden, sind in der Regel unveränderlich.
Effiziente Bitmap-Verarbeitung
Bitmap-Pooling und ‑Wiederverwendung
Das häufige Zuweisen und Freigeben von Bitmaps führt zu Zuweisungs-Churn, wodurch die Garbage Collection ständig ausgeführt wird. Gängige Bibliotheken zum Laden von Bildern verwenden einen Bitmap-Pool.
Google empfiehlt Glide für Java-basierte Anwendungen und Coil für Kotlin-basierte Anwendungen (insbesondere bei Verwendung von Jetpack Compose).
Wenn eine Bitmap nicht mehr benötigt wird, ruft die App bitmap.recycle() auf oder gibt sie an einen Pool zurück, anstatt sie vom Garbage Collector löschen zu lassen. Wenn das nächste Mal ein Bitmap mit denselben Abmessungen und derselben Konfiguration benötigt wird, stellt der Pool den vorhandenen Puffer bereit, sodass keine neue Zuweisung erforderlich ist.
Hardware-Bitmaps
Mit Bitmap.Config.HARDWARE können Sie Pixeldaten direkt im Grafikspeicher (DMABuf) speichern.
- Vorteile:
- Speichereinsparungen: Es wird kein Anwendungs- oder nativer Heap verwendet, sondern GPU-Arbeitsspeicher. Oft müssen Bitmaps, die in der Benutzeroberfläche einer App angezeigt werden, ohnehin in den GPU-Speicher kopiert werden. Dadurch wird dieser Kopiervorgang und der zusätzliche Speicherbedarf vermieden.
- Leistung: Extrem schnell, da die Daten bereits auf der GPU sind.
- Nachteile:
- Unveränderlich: Hardware-Bitmaps können nicht geändert werden.
- Rücklesen ist langsam: Der Zugriff auf Pixel über die CPU (z.B.
getPixel()) ist sehr aufwendig. - Attribution: In Standardtools wie AHAT (siehe unten) schwieriger nachzuvollziehen.
Häufige Fallstricke bei der Bitmap-Arbeitsspeichernutzung
Auch wenn Sie moderne Bitmap-Konfigurationen verwenden, können mehrere wiederkehrende Muster beim Decodieren und Planen von Bitmaps zu großen Speicherspitzen führen.
Dekodieren von überdimensionierten Bitmaps
Ein Foto mit voller Auflösung (4.000 × 3.000 Pixel) belegt in ARGB_8888 48 MB. Das vollständige Bild zu dekodieren, um es in einer Miniaturansicht mit 200 × 150 Pixeln zu rendern, verschwendet über 99% des zugewiesenen Pixelpuffers.
Wenn Sie Bilder direkt mit ImageDecoder oder BitmapFactory decodieren, reduzieren Sie die Auflösung während des Decodierungsvorgangs mit ImageDecoder.setTargetSize() oder BitmapFactory.Options.inSampleSize, damit sie den Abmessungen der Zielansicht entspricht. Bibliotheken zum Laden von Bildern wie Glide und Coil führen dieses Downsampling automatisch durch, wenn Sie eine begrenzte Zielansichtsgröße angeben.
Wenn Sie beispielsweise ein Bitmap mit ImageDecoder decodieren, übergeben Sie ein OnHeaderDecodedListener, das die Ausgabedimensionen auf die Größe der Zielansicht skaliert:
val source = ImageDecoder.createSource(resources, R.drawable.high_res_photo)
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
if (info.size.width > targetWidth || info.size.height > targetHeight) {
decoder.setTargetSize(targetWidth, targetHeight)
}
}
Hohe gleichzeitige Zuschauerbindung durch parallele Dekodierung
Auch wenn einzelne Bitmaps die richtige Größe haben und nur kurzlebig sind, kann das parallele Decodieren vieler Bilder zu starken Speicherspitzen führen. Wenn beispielsweise auf einem Bildschirm für Organisatoren oder Galerien 30 Aufgaben in einem unbegrenzten Thread-Pool gleichzeitig zum Decodieren von Symbolen oder Thumbnails gesendet werden, belegen alle 30 unkomprimierten Pixelpuffer und Decoder-Scratch-Puffer gleichzeitig RAM.
Diese hohe gleichzeitige Aufbewahrung erhöht den maximalen nativen Heap-Speicherbedarf und kann lmkd-Vorgänge auslösen, bevor der Batch abgeschlossen ist. Begrenzen Sie die Dekodierungskonkurrenz mit einem eingeschränkten Thread-Pool, einem Semaphor oder einem Coroutine-Dispatcher wie Dispatchers.IO.limitedParallelism(2), damit nur wenige Bitmaps gleichzeitig dekodiert werden.
Nicht wiederverwendete temporäre Bitmaps in Frame-Verarbeitungsschleifen
Unter Android 8.0 und höher belegt das Java-Wrapper-Objekt Bitmap nur etwa 56 Byte im Java-Heap, während sich der Pixelpuffer im nativen Heap befindet und mehrere Megabyte belegen kann. Sie können diese Aufteilung im Android Studio-Speicher-Profiler oder in AHAT überprüfen. Dort hat jede Bitmap-Instanz eine flache Java-Größe von etwa 56 Byte neben ihrer nativen Größe von mehreren Megabyte. In dumpsys meminfo finden Sie die Aufteilung unter Native Allocations (Bitmap (malloced)).
Bei Pipelines mit hoher Frequenz wie der Analyse von Kamerabildern, OCR oder ML-Inferenzschleifen wird häufig für jedes Bild eine neue Bitmap zugewiesen, indem ImageProxy.toBitmap() und Bitmap.createBitmap() für die Drehung oder das Zuschneiden aufgerufen werden.
Wenn Sie Verweise auf abgelöste Frames entfernen, ohne sie wiederzuverwenden, kann der native Arbeitsspeicher stark ansteigen. Die winzigen Java-Wrapper erhöhen die Java-Heap-Belegung kaum. Daher wird die automatische Speicherbereinigung nicht schnell genug ausgelöst, um zu verhindern, dass sich Hunderte von Megabyte an nativen Pixelpuffern ansammeln, bevor NativeAllocationRegistry sie zurückfordert.
Wenn Sie Frames in einer engen Schleife verarbeiten, verwenden Sie nach Möglichkeit vorab zugewiesene Puffer wieder oder rufen Sie bitmap.recycle() für temporäre Zwischen-Bitmaps auf, sobald die Verarbeitung jedes Frames abgeschlossen ist.
Praktische Übung: Bitmap-Untersuchung
Wir verwenden die Beispiel-App BitmapLab, um diese Konzepte zu veranschaulichen.
1. Messen mit dumpsys meminfo
Starte BitmapLab und tippe auf ALLOCATE 10MB ARGB_8888 (10 MB ARGB_8888 zuweisen). Führen Sie dann diesen Befehl aus:
adb shell dumpsys meminfo -s com.android.bitmaplab
Suchen Sie in modernen Android-Versionen nach dem Abschnitt Native Allocations (Native Zuweisungen). Diese bieten eine viel bessere Zuordnung für Bitmaps als die allgemeine App-Zusammenfassung:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- Bitmap (malloced): Bitmaps, die im nativen Heap des Prozesses zugewiesen wurden. Hier befinden sich die meisten Standard-Bitmaps in Android 8.0 und höher.
- Bitmap (nonmalloced): Bitmaps, die speziellen Speicher wie Hardware-Bitmaps oder Shared Bitmaps (über
ashmemodermemfd) verwenden.
Wenn Sie in BitmapLab eine gemeinsame Bitmap zuweisen, wird sie in Bitmap (nonmalloced) angezeigt:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240
Bitmap (nonmalloced): 1 10240 # <--- Shared Bitmap!
Tracking von gemeinsam genutzten Bitmaps
In einigen Android-Versionen und Kernelkonfigurationen bietet dumpsys meminfo auch eine hochauflösende Erfassung für Bitmaps, die über Dateideskriptoren in den Adressraum des Prozesses gemappt werden.
Standardmäßig wird für freigegebene Bitmaps ein generischer Name („bitmap“) verwendet. Wenn Sie eine detaillierte Zuordnung und das Tracking eindeutiger Bitmaps (Identifizieren freigegebener Bitmaps in verschiedenen Prozessen) aktivieren möchten, müssen Sie das folgende Systemattribut aktivieren:
adb shell setprop debug.hwui.bitmap_ashmem_long_name true
Wenn diese Option aktiviert ist, haben die ashmem-Regionen in /proc/<pid>/smaps beschreibendere Namen. meminfo nutzt das und die Ergebnisse sehen so aus:
Shared Bitmaps
Count Size(KB)
------ ------
Mapped: 1 10240
Unique: 1 10240
- Zugeordnet: Die Gesamtgröße aller Bitmap-bezogenen Speicherzuordnungen.
- Eindeutig: Die Größe von Bitmaps, bei denen nur eindeutige Werte berücksichtigt werden. Das bedeutet, dass zwei oder mehr Zuordnungen derselben zugrunde liegenden gemeinsamen Bitmap-Pixeldaten nur einmal gezählt werden.
2. Bitmaps in AHAT
AHAT bietet eine hervorragende Visualisierung für Bitmaps.
- Weisen Sie in BitmapLab einige Bitmaps zu.
Erstellen Sie einen Heap-Dump mit dem Flag
-b(um native Bitmap-Daten einzuschließen):adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof adb pull /data/local/tmp/bitmaps.hprof . ahat bitmaps.hprofÖffnen Sie
localhost:7100und suchen Sie in der Seitenleiste nach dem Link Bitmaps oder suchen Sie nach der KlasseBitmap.AHAT rendert die Bitmaps im Browser, sodass Sie ganz einfach erkennen können, welche Bilder viel Speicherplatz belegen.

3. Bitmap-Tracks in Perfetto
Mit Perfetto können Bitmap-Zuweisungen und ‑Anzahlen im Zeitverlauf verfolgt werden. Diese Zähler werden vom Android-Framework ausgegeben, wenn die atrace-Kategorie gfx für eine bestimmte Anwendung aktiviert ist.
Starten Sie einen Trace. Sie müssen die Kategorie
gfxangeben und mit dem Flag-aauf das jeweilige App-Paket ausrichten:external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \ -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplabTippen Sie in BitmapLab wiederholt auf die Schaltflächen Zuweisen und Löschen.
Tippen Sie auch auf Bitmap für Paket/Entpacken.
Analysieren Sie den Trace in ui.perfetto.dev.
Im Prozessabschnitt für com.android.bitmaplab sehen Sie Folgendes:
* Bitmap Count (Bitmap-Anzahl): Ein Zähler, der die Anzahl der aktiven Bitmaps angibt.
* Bitmap-Arbeitsspeicher: Ein Zähler, der die insgesamt von Bitmaps verwendeten Byte angibt.
Slices auf hoher Ebene (Perfetto SDK)
BitmapLab verwendet auch das Perfetto SDK, um übergeordnete Slices für Bitmap-Vorgänge auszugeben. Suchen Sie im Trace nach BitmapLab_, um Folgendes zu finden:
* BitmapLab_parcelUnparcel: Slices, die die Logik für das Aufteilen und Zusammenführen von Paketen abdecken.
* BitmapLab_postNotification: Segmente für den Ablauf zum Posten von Benachrichtigungen.
Benachrichtigungsabläufe verfolgen
Wenn Sie auf Benachrichtigung posten tippen, erstellt die App eine Benachrichtigung mit der aktuellen Bitmap und sendet sie an das System. Der Framework-Code, der dafür verantwortlich ist, gibt Perfetto-Slices mit Flow-Ereignissen aus, die das Parcelling (Bitmap in ein Parcel schreiben, das über Binder IPC gesendet werden soll) und das Unparcelling (Bitmap am Empfangsende aus einem Parcel lesen) verbinden.
Im Screenshot unten sehen Sie, wie die App das große Bitmap für die Verwendung in einer Binder-Transaktion zum Posten der Benachrichtigung parzelliert und wie es im system_server-Prozess entparzelliert wird.

Mit Perfetto können Sie sogar derselben Benachrichtigungs-Bitmap folgen, wenn sie sich über Threads und Prozesse hinweg weiter ausbreitet, z. B. von einem Binder-Thread in system_server (das den INotificationManager-Binderserver implementiert) zu system_server-Worker-Threads, die dieselbe Bitmap dann möglicherweise an com.android.systemui weiterleiten, damit sie in der Benachrichtigungsleiste angezeigt wird.
Herausforderungen bei System-Apps
System-Apps wie SystemUI (Benachrichtigungen) und Launcher stehen vor besonderen Herausforderungen:
- Unbegrenzte Inhalte: Benachrichtigungen und Widgets können zahlreich sein. Wenn jede eine große Bitmap enthält, kann das System schnell nicht mehr genügend Arbeitsspeicher haben.
- Duplizierung: Das gleiche App-Symbol kann im Cache des Launchers, im Benachrichtigungsbereich von SystemUI und in den Einstellungen gespeichert sein.
- Freigabe über Hardwarepuffer: Um dieses Problem zu beheben, werden Systemkomponenten auf einen zentralen „Image Offload“-Dienst umgestellt, der
HardwareBuffer-Instanzen prozessübergreifend freigibt. DMABuf-Attribution: Hardware-Bitmaps sparen Heapspeicher, verwenden aber DMABuf-Speicher, der mit Standard-Speichertools schwieriger einem bestimmten Prozess zuzuordnen ist.
Verwenden Sie
adb shell dmabuf_dump, um systemweite DMABuf-Zuweisungen zu sehen. Dieses Tool bietet eine Aufschlüsselung der Puffer nach Prozess:droid.bitmaplab:19562 Name Rss Pss nr_procs Inode Exporter <unknown> 3840 kB 1280 kB 3 3397 virtio_gpu system 12 kB 4 kB 3 3398 system <unknown> 3840 kB 1920 kB 2 3399 virtio_gpu system 12 kB 6 kB 2 3400 system PROCESS TOTAL 11556 kB 5136 kB- RSS: Die Gesamtgröße des Puffers, wenn er im Prozess zugeordnet ist.
- Pss: Die proportionale Größe (RSS geteilt durch die Anzahl der Prozesse, die den Puffer gemeinsam nutzen). Das ist der beste Messwert für die Buchhaltung.
- nr_procs: Die Anzahl der Prozesse, die derzeit eine Referenz auf diesen Puffer haben.
- Exporter: Der Treiber, der den Puffer zugewiesen hat (z.B.
virtio_gpuauf Cuttlefish oder ein anbieterspezifischer Ion-/DMA-BUF-Heap auf der Hardware).
Mit
adb shell dmabuf_dump -berhalten Sie eine Zusammenfassung aller Puffer und der gesamten systemweiten DMA-BUF-Nutzung.
← Java | ↑ Nach oben | Nativ →