Obiekty bitmapowe często w największym stopniu przyczyniają się do wykorzystania pamięci przez aplikację. Niezależnie od tego, czy są to ikony aplikacji, obrazy powiadomień czy treści multimedialne, nieefektywne zarządzanie bitmapami może szybko doprowadzić do błędów braku pamięci (OOM) i obciążenia pamięci w całym systemie.
Konfiguracje bitmap i dane pikseli
Ilość pamięci zużywanej przez bitmapę zależy przede wszystkim od jej wymiarów (szerokość × wysokość) i konfiguracji (Bitmap.Config).
Konfiguracja określa, ile bajtów jest używanych do reprezentowania każdego piksela:
| Konfiguracja | Bajty na piksel | Opis |
|---|---|---|
ALPHA_8 |
1 | Tylko kanał alfa (przezroczystość). Przydatne w przypadku masek. |
RGB_565 |
2 | Czerwony (5 bitów), zielony (6 bitów), niebieski (5 bitów). Brak kanału alfa. Dobre rozwiązanie w przypadku nieprzezroczystych obrazów, w których wysoka wierność kolorów nie jest najważniejsza. |
ARGB_8888 |
4 | Kanał alfa, czerwony, zielony, niebieski (po 8 bitów). Domyślna i najczęściej używana konfiguracja. |
RGBA_F16 |
8 | Liczba zmiennoprzecinkowa o połowie precyzji. Używana w przypadku treści HDR i treści o szerokiej gamie kolorów. |
HARDWARE |
Nie dotyczy | Przechowywana w pamięci karty graficznej (gralloc/DMABuf). Zobacz Bitmapy sprzętowe. |
Wzór na ilość pamięci: Memory (Bytes) = Width × Height × Bytes Per Pixel
Na przykład obraz na pełnym ekranie urządzenia 1080p (1920 × 1080) w konfiguracji ARGB_8888 zajmuje: 1920 × 1080 × 4 bajty ≈ 8, 3 MB.
Bitmapy sterty a bitmapy współdzielone
Bitmapy sterty (sterta natywna)
W nowoczesnym Androidzie (8.0 lub nowszym) dane pikseli bitmap są przechowywane w stercie natywnej, a tylko mały obiekt otoki znajduje się w stercie Javy.
Gdy aplikacja musi wyświetlić obraz, jest on zwykle dekodowany ze skompresowanego pliku obrazu do bitmapy i przechowywany w stercie.
Bitmapy współdzielone (ashmem/memfd)
Gdy bitmapa jest przesyłana między procesami (np. przez Binder do SystemUI w przypadku powiadomienia), Android unika kopiowania danych pikseli, używając pamięci współdzielonej (ashmem lub memfd).
Instancję Bitmap można skopiować do pamięci współdzielonej jawnie, wywołując
Bitmap.asShared(),
lub niejawnie, jeśli Bitmap zostanie umieszczona w Parcel (zwykle przez dodanie
bitmapy do Parcelable, np. Bundle) i wysłana przez Binder IPC.
Gdy współdzielona bitmapa jest wysyłana przez Binder IPC, same dane pikseli nie są kopiowane, ale deskryptor pliku odwołujący się do regionu pamięci współdzielonej jest duplikowany do procesu odbiorcy. Podstawowy region pamięci może być współdzielony przez wiele procesów i nie jest zwalniany, dopóki wszystkie deskryptory plików odwołujące się do niego nie zostaną zamknięte.
Bitmapy modyfikowalne i niemodyfikowalne
- Bitmapy modyfikowalne: można je modyfikować po utworzeniu (np. za pomocą
Canvas). Zawsze wymagają własnej prywatnej alokacji pamięci. Jeśli modyfikowalna bitmapa zostanie skopiowana, należy utworzyć jej kopię głęboką (drugą kopię wszystkich danych pikseli). - Bitmapy niemodyfikowalne: nie można ich zmieniać. Umożliwia to optymalizacje, takie jak współdzielenie tego samego podstawowego bufora pamięci między różnymi instancjami
Bitmap. Bitmapy wczytywane z zasobów APK (BitmapFactory) są zwykle niemodyfikowalne.
Efektywne zarządzanie bitmapami
Pula bitmap i ponowne użycie
Częste alokowanie i zwalnianie bitmap powoduje churn alokacji, co zmusza GC do ciągłego działania. Popularne biblioteki wczytywania obrazów używają puli bitmap.
Google zaleca używanie Glide w przypadku aplikacji opartych na Javie i Coil w przypadku aplikacji opartych na Kotlinie (szczególnie w przypadku korzystania z Jetpack Compose).
Gdy bitmapa nie jest już potrzebna, aplikacja wywołuje bitmap.recycle() lub zwraca ją do puli, zamiast pozwolić na jej usunięcie przez GC. Gdy następnym razem potrzebna będzie bitmapa o tych samych wymiarach i konfiguracji, pula udostępni istniejący bufor, co pozwoli uniknąć nowej alokacji.
Bitmapy sprzętowe
Bitmap.Config.HARDWARE umożliwia przechowywanie danych pikseli bezpośrednio w pamięci graficznej (DMABuf).
- Zalety:
- Oszczędność pamięci: nie używa sterty aplikacji ani sterty natywnej, tylko pamięci GPU. Bitmapy wyświetlane w interfejsie aplikacji i tak muszą być kopiowane do pamięci GPU, więc pozwala to zaoszczędzić operację kopiowania i dodatkowe koszty pamięci.
- Wydajność: bardzo szybkie rysowanie, ponieważ dane są już na GPU.
- Wady:
- Niemodyfikowalne: bitmap sprzętowych nie można modyfikować.
- Powolny odczyt: dostęp do pikseli z procesora (np.
getPixel()) jest bardzo kosztowny. - Atrybucja: trudniejsza do śledzenia w standardowych narzędziach, takich jak AHAT (patrz poniżej).
Ćwiczenie praktyczne: eksploracja bitmap
Do zbadania tych koncepcji użyjemy przykładowej aplikacji BitmapLab.
1. Pomiar za pomocą dumpsys meminfo
Uruchom BitmapLab i kliknij ALLOCATE 10MB ARGB_8888. Następnie uruchom:
adb shell dumpsys meminfo -s com.android.bitmaplab
W nowoczesnych wersjach Androida poszukaj sekcji Native Allocations. Zapewniają one znacznie lepszą atrybucję bitmap niż ogólna sekcja App Summary:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- Bitmap (malloced): bitmapy przydzielone w stercie natywnej procesu. W Androidzie 8.0 lub nowszym większość standardowych bitmap znajduje się w tym miejscu.
- Bitmap (nonmalloced): bitmapy, które używają specjalnej pamięci, np.
bitmap sprzętowych lub bitmap współdzielonych (za pomocą
ashmemlubmemfd).
Jeśli w BitmapLab przydzielisz współdzieloną bitmapę, zobaczysz ją
w Bitmap (nonmalloced):
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240
Bitmap (nonmalloced): 1 10240 # <--- Shared Bitmap!
Śledzenie bitmap współdzielonych
W niektórych wersjach Androida i konfiguracjach jądra dumpsys meminfo zapewnia też śledzenie w wysokiej rozdzielczości bitmap, które są mapowane do przestrzeni adresowej procesu za pomocą deskryptorów plików.
Domyślnie bitmapy współdzielone używają ogólnej nazwy („bitmap”). Aby włączyć szczegółową atrybucję i unikalne śledzenie bitmap (identyfikowanie bitmap współdzielonych w różnych procesach), musisz włączyć tę właściwość systemu:
adb shell setprop debug.hwui.bitmap_ashmem_long_name true
Po włączeniu regiony ashmem w /proc/<pid>/smaps będą miały bardziej
opisowe nazwy. meminfo wykorzysta to, a wyniki będą wyglądać tak:
Shared Bitmaps
Count Size(KB)
------ ------
Mapped: 1 10240
Unique: 1 10240
- Mapped: łączny rozmiar wszystkich mapowań pamięci związanych z bitmapami.
- Unikalne: rozmiar bitmap, który uwzględnia tylko unikalne mapowania (czyli 2 lub więcej mapowań tych samych podstawowych danych pikseli bitmapy współdzielonej liczy się tylko raz).
2. Bitmapy w AHAT
AHAT zapewnia doskonałą wizualizację bitmap.
- W BitmapLab przydziel kilka bitmap.
Zrób zrzut sterty za pomocą flagi
-b(aby uwzględnić natywne dane bitmapy):adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof adb pull /data/local/tmp/bitmaps.hprof . ahat bitmaps.hprofOtwórz
localhost:7100i w pasku bocznym poszukaj linku Bitmaps lub wyszukaj klasęBitmap.AHAT renderuje bitmapy w przeglądarce, co ułatwia identyfikowanie obrazów, które zajmują dużo pamięci.

3. Ślady bitmap w Perfetto
Perfetto może śledzić alokacje i liczbę bitmap w czasie. Te liczniki są emitowane przez platformę Androida, gdy w przypadku konkretnej aplikacji włączona jest kategoria śledzenia gfx.
Rozpocznij śledzenie. Musisz uwzględnić kategorię
gfxi określić pakiet aplikacji za pomocą flagi-a:external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \ -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplabW BitmapLab wielokrotnie klikaj przyciski Allocate i Clear.
Kliknij też Parcel/Unparcel Bitmap.
Przeanalizuj ślad w ui.perfetto.dev.
W sekcji procesu com.android.bitmaplab zobaczysz te informacje:
* Bitmap Count: licznik pokazujący liczbę aktywnych bitmap.
* Bitmap Memory: licznik pokazujący łączną liczbę bajtów używanych przez bitmapy.
Fragmenty wysokiego poziomu (Perfetto SDK)
BitmapLab używa też Perfetto SDK do emitowania fragmentów wysokiego poziomu w przypadku operacji na bitmapach. Aby znaleźć te informacje, wyszukaj w śladzie BitmapLab_:
* BitmapLab_parcelUnparcel: fragmenty obejmujące logikę pakowania i rozpakowywania.
* BitmapLab_postNotification: fragmenty obejmujące proces publikowania powiadomień.
Śledzenie procesów powiadomień
Gdy klikniesz Post Notification, aplikacja utworzy powiadomienie zawierające bieżącą bitmapę i wyśle je do systemu. Kod platformy odpowiedzialny za to emituje fragmenty Perfetto ze zdarzeniami przepływu, które łączą pakowanie (zapisywanie bitmapy do Parcel w celu wysłania przez Binder IPC) i rozpakowywanie (odczytywanie bitmapy z Parcel po stronie odbiorcy).
Na zrzucie ekranu poniżej widać, jak aplikacja pakuje dużą bitmapę, która ma być używana w transakcji Binder do opublikowania powiadomienia, oraz odpowiadające jej rozpakowywanie w procesie system_server.

Za pomocą Perfetto możesz nawet śledzić tę samą bitmapę powiadomienia, gdy jest ona dalej propagowana między wątkami i procesami, np. z wątku Binder w system_server (który implementuje serwer Binder INotificationManager) do wątków roboczych system_server, które mogą następnie przekazać tę samą bitmapę do com.android.systemui, aby wyświetlić ją w obszarze powiadomień.
Problemy z aplikacjami systemowymi
Aplikacje systemowe, takie jak SystemUI (powiadomienia) i Launcher , mają unikalne problemy:
- Nieograniczona ilość treści: powiadomień i widżetów może być wiele. Jeśli każdy z nich zawiera dużą bitmapę, system może szybko wyczerpać pamięć.
- Duplikacja: ta sama ikona aplikacji może być przechowywana w pamięci podręcznej Launchera, obszarze powiadomień SystemUI i aplikacji Ustawienia.
- Udostępnianie za pomocą buforów sprzętowych: aby temu zapobiec, komponenty systemu
przechodzą na scentralizowaną usługę „odciążania obrazów”, która współdzieli
HardwareBufferinstancje między procesami. Atrybucja DMABuf: bitmapy sprzętowe oszczędzają miejsce w stercie, ale używają pamięci DMABuf , którą trudniej przypisać do konkretnego procesu w standardowych narzędziach do zarządzania pamięcią.
Aby zobaczyć alokacje DMABuf w całym systemie, użyj
adb shell dmabuf_dump. To narzędzie zawiera zestawienie buforów na procesy: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: łączny rozmiar bufora, jeśli jest on mapowany w procesie.
- Pss: proporcjonalny rozmiar (RSS podzielony przez liczbę procesów współdzielących bufor). To najlepsza metryka do rozliczania.
- nr_procs: liczba procesów, które obecnie mają odniesienie do tego bufora.
- Eksporter: sterownik, który przydzielił bufor (np.
virtio_gpuw Cuttlefish lub sterta Ion/DMA-BUF specyficzna dla dostawcy w sprzęcie).
Możesz też użyć
adb shell dmabuf_dump -b, aby uzyskać podsumowanie wszystkich buforów i łącznego wykorzystania DMA-BUF w całym systemie.