Bitmaps und Arbeitsspeicher

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 Fehlern aufgrund von zu wenig Arbeitsspeicher (Out-Of-Memory, OOM) und zu einer systemweiten Speicherauslastung führen.

Bitmap-Konfigurationen und Pixeldaten

Die Menge an Arbeitsspeicher, die eine Bitmap verbraucht, wird hauptsächlich durch ihre Abmessungen (Breite × Höhe) und ihre Konfiguration (Bitmap.Config) bestimmt.

Die Konfiguration definiert, wie viele Byte zur Darstellung jedes Pixels verwendet werden:

Konfiguration Byte 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 undurchsichtige Bilder, bei denen eine hohe Farbtreue nicht entscheidend ist.
ARGB_8888 4 Alpha, Rot, Grün, Blau (jeweils 8 Bit). Standard und am häufigsten verwendet.
RGBA_F16 8 Gleitkomma mit halber Genauigkeit. Wird für Inhalte mit großem Farbraum und HDR verwendet.
HARDWARE Im Grafikspeicher gespeichert (gralloc/DMABuf). Weitere Informationen zu Hardware-Bitmaps.

Speicherformel: 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 Byte ≈ 8, 3 MB.

Heap-Bitmaps und freigegebene Bitmaps

Heap-Bitmaps (nativer Heap)

In modernen Android-Versionen (8.0 und höher) werden Bitmap-Pixeldaten im nativen Heap, während sich nur ein kleines Wrapper-Objekt im Java-Heap befindet.

Wenn eine App ein Bild präsentieren muss, wird es normalerweise aus einer komprimierten Bilddatei in eine Bitmap decodiert und im Heap gespeichert.

Freigegebene Bitmaps (ashmem/memfd)

Wenn eine Bitmap zwischen Prozessen übertragen wird (z.B. über Binder an SystemUI für eine Benachrichtigung), vermeidet Android das Kopieren der Pixeldaten, indem gemeinsam genutzter Speicher (ashmem oder memfd) verwendet wird.

Eine Bitmap-Instanz kann explizit in den gemeinsam genutzten Speicher kopiert werden, indem Bitmap.asShared() aufgerufen wird, oder implizit, wenn eine Bitmap in ein Parcel eingefügt wird (in der Regel durch Hinzufügen der Bitmap zu einem Parcelable wie einem Bundle) und über Binder IPC gesendet wird.

Wenn eine freigegebene 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 Dateideskriptoren, die darauf verweisen, 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 tiefe 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 geladen wurden (BitmapFactory), 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 (GC) ständig ausgeführt wird. Gängige Bibliotheken zum Laden von Bildern verwenden einen Bitmap-Pool.

Google empfiehlt Glide als Lösung 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 von der GC freigeben zu lassen. Wenn das nächste Mal eine Bitmap mit denselben Abmessungen und derselben Konfiguration benötigt wird, stellt der Pool den vorhandenen Puffer bereit, wodurch eine neue Zuweisung vermieden wird.

Hardware-Bitmaps

Mit Bitmap.Config.HARDWARE können Sie Pixeldaten direkt im Grafikspeicher (DMABuf) speichern.

  • Vorteile:
    • Speichereinsparungen: Verwendet keinen Anwendungs- oder nativen Heap, sondern GPU Speicher. Oft müssen Bitmaps, die in der UI einer App angezeigt werden, ohnehin in den GPU-Speicher kopiert werden. Dadurch wird dieser Kopiervorgang und die zusätzlichen Speicherkosten vermieden.
    • Leistung: Extrem schnell zu zeichnen, da sich die Daten bereits auf der GPU befinden.
  • Nachteile:
    • Unveränderlich: Hardware-Bitmaps können nicht geändert werden.
    • Langsames Zurücklesen: Der Zugriff auf Pixel von der CPU aus (z.B. getPixel()) ist sehr aufwendig.
    • Attribution: In Standardtools wie AHAT (siehe unten) schwieriger zu verfolgen.

Praktische Übung: Bitmap-Untersuchung

Wir verwenden die Beispiel-App BitmapLab , um diese Konzepte zu untersuchen.

1. Messung mit dumpsys meminfo

Starten Sie BitmapLab und tippen Sie auf ALLOCATE 10MB ARGB_8888. 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. Diese bieten eine viel bessere Attribution für Bitmaps als die allgemeine App Summary:

 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 verwenden, z. B. Hardware-Bitmaps oder freigegebene Bitmaps (über ashmem oder memfd).

Wenn Sie in BitmapLab eine freigegebene Bitmap zuweisen, wird sie in Bitmap (nonmalloced) angezeigt:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240
Bitmap (nonmalloced):        1                          10240  # <--- Shared Bitmap!

Tracking freigegebener Bitmaps

In einigen Android-Versionen und Kernel-Konfigurationen bietet dumpsys meminfo auch eine hochauflösende Verfolgung von Bitmaps, die über Dateideskriptoren in den Adressraum des Prozesses eingebunden werden.

Standardmäßig verwenden freigegebene Bitmaps einen generischen Namen („bitmap“). Wenn Sie eine detaillierte Attribution und eine eindeutige Bitmap-Verfolgung aktivieren möchten (um freigegebene Bitmaps in verschiedenen Prozessen zu identifizieren), müssen Sie die folgende Systemeigenschaft aktivieren:

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

Wenn diese Option aktiviert ist, haben die ashmem-Regionen in /proc/<pid>/smaps aussagekräftigere Namen. meminfo nutzt dies und die Ergebnisse sehen so aus:

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • Mapped: Die Gesamtgröße aller speicherbezogenen Bitmap-Zuordnungen.
  • Unique: Die Größe von Bitmaps, wobei nur eindeutige Zuordnungen berücksichtigt werden (d.h. zwei oder mehr Zuordnungen derselben zugrunde liegenden freigegebenen Bitmap-Pixeldaten werden nur einmal gezählt).

2. Bitmaps in AHAT

AHAT bietet eine hervorragende Visualisierung für Bitmaps.

  1. Weisen Sie in BitmapLab einige Bitmaps zu.
  2. Erfassen Sie einen Heap-Dump mit dem Flag -b (um native Bitmap-Daten einzubeziehen):

    adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof
    adb pull /data/local/tmp/bitmaps.hprof .
    ahat bitmaps.hprof
    
  3. Öffnen Sie localhost:7100 und suchen Sie in der Seitenleiste nach dem Link Bitmaps oder suchen Sie nach der Klasse Bitmap.

  4. AHAT rendert die Bitmaps im Browser, sodass Sie leicht erkennen können, welche Bilder viel Arbeitsspeicher belegen.

AHAT mit gerenderten Bitmaps

3. Bitmap-Tracks in Perfetto

Mit Perfetto lassen sich Bitmap-Zuweisungen und -Anzahlen im Zeitverlauf verfolgen. Diese Zähler werden vom Android-Framework ausgegeben, wenn die Atrace-Kategorie gfx für eine bestimmte Anwendung aktiviert ist.

  1. Starten Sie einen Trace. Sie müssen die Kategorie gfx einbeziehen und das spezifische App-Paket mit dem Flag -a angeben:

    external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \
        -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplab
    
  2. Tippen Sie in BitmapLab wiederholt auf die Schaltflächen Allocate und Clear.

  3. Tippen Sie auch auf Parcel/Unparcel Bitmap.

  4. Analysieren Sie den Trace in ui.perfetto.dev.

Im Prozessabschnitt für com.android.bitmaplab sehen Sie Folgendes: * Bitmap Count: Ein Zähler, der die Anzahl der aktiven Bitmaps anzeigt. * Bitmap Memory: Ein Zähler, der die von Bitmaps verwendeten Byte insgesamt anzeigt.

Slices auf hoher Ebene (Perfetto SDK)

BitmapLab verwendet auch das Perfetto SDK , um Slices auf hoher Ebene für Bitmap-Vorgänge auszugeben. Suchen Sie im Trace nach BitmapLab_. Dort finden Sie: * BitmapLab_parcelUnparcel: Slices, die die Logik für das Parceling und Unparceling abdecken. * BitmapLab_postNotification: Slices, die den Ablauf zum Posten von Benachrichtigungen abdecken.

Tracking von Benachrichtigungsabläufen

Wenn Sie auf Post Notification 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 Ablaufereignissen aus, die das Parceling (Schreiben der Bitmap in ein Parcel, das über Binder IPC gesendet werden soll) und das Unparceling (Lesen der Bitmap aus einem Parcel auf der Empfängerseite) verbinden.

Im Screenshot unten sehen Sie, wie die App die große Bitmap für die Verwendung in einer Binder-Transaktion zum Posten der Benachrichtigung parselt, und das entsprechende Unparceling im Prozess system_server.

Perfetto-Diagramm, das einen Fluss von BitmapLab zu system_server über Notification zeigt

Mit Perfetto können Sie sogar dieselbe Benachrichtigungs-Bitmap verfolgen, während sie sich weiter über Threads und Prozesse hinweg ausbreitet, z. B. von einem Binder-Thread in system_server (der den Binder-Server INotificationManager implementiert) zu system_server-Worker-Threads, die dieselbe Bitmap dann an com.android.systemui weiterleiten können, damit sie im Benachrichtigungsbereich angezeigt wird.

Herausforderungen bei System-Apps

System-Apps wie SystemUI (Benachrichtigungen) und Launcher stehen vor besonderen Herausforderungen:

  1. Unbegrenzte Inhalte: Es kann viele Benachrichtigungen und Widgets geben. Wenn jedes eine große Bitmap enthält, kann das System schnell Out of Memory sein.
  2. Duplizierung: Dasselbe App-Symbol kann sich im Cache des Launchers, im Benachrichtigungsbereich von SystemUI und in der App „Einstellungen“ befinden.
  3. Freigabe über Hardware-Puffer: Um dies zu vermeiden, werden Systemkomponenten auf einen zentralen Dienst zum Auslagern von Bildern umgestellt, der HardwareBufferInstanzen prozessübergreifend freigibt.
  4. DMABuf-Attribution: Hardware-Bitmaps sparen Heap-Speicher, verwenden aber DMABuf Speicher, der in 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 Prozessen:

     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 eingebunden ist.
    • Pss: Die proportionale Größe (RSS geteilt durch die Anzahl der Prozesse, die den Puffer gemeinsam nutzen). Dies ist die beste Messung für die Abrechnung.
    • nr_procs: Die Anzahl der Prozesse, die derzeit einen Verweis auf diesen Puffer haben.
    • Exporter: Der Treiber, der den Puffer zugewiesen hat (z.B. virtio_gpu auf Cuttlefish oder ein anbieterspezifischer Ion/DMA-BUF-Heap auf Hardware).

    Sie können auch adb shell dmabuf_dump -b verwenden, um eine Zusammenfassung aller Puffer und die gesamte systemweite DMA-BUF-Nutzung zu sehen.


← Java | ↑ Nach oben | Native →