Gli oggetti bitmap sono spesso i maggiori contributori singoli al footprint della memoria di un'applicazione. Che si tratti di icone delle app, immagini di notifiche o contenuti multimediali, la gestione inefficiente delle bitmap può portare rapidamente a errori di memoria insufficiente e a una pressione sulla memoria a livello di sistema.
Configurazioni bitmap e dati pixel
La quantità di memoria consumata da una bitmap è determinata principalmente dalle sue dimensioni (larghezza × altezza) e dalla sua configurazione (Bitmap.Config).
La configurazione definisce il numero di byte utilizzati per rappresentare ogni pixel:
| Configurazione | Byte per pixel | Descrizione |
|---|---|---|
ALPHA_8 |
1 | Solo canale alfa (trasparenza). Utile per le maschere. |
RGB_565 |
2 | Rosso (5 bit), verde (6 bit), blu (5 bit). Nessun alfa. Ideale per immagini opache in cui l'alta fedeltà dei colori non è fondamentale. |
ARGB_8888 |
4 | Alfa, rosso, verde, blu (8 bit ciascuno). Predefinita e più comune. |
RGBA_F16 |
8 | Virgola mobile a metà precisione. Utilizzata per contenuti HDR e con ampia gamma di colori. |
HARDWARE |
N/D | Archiviata nella memoria video (gralloc/DMABuf). Consulta Bitmap hardware. |
Formula della memoria: Memory (Bytes) = Width × Height × Bytes Per Pixel
Ad esempio, un'immagine a schermo intero su un dispositivo 1080p (1920x1080) in ARGB_8888 occupa: 1920 × 1080 × 4 byte ≈ 8,3 MB.
Bitmap heap e bitmap condivise
Bitmap heap (heap nativo)
Nelle versioni moderne di Android (8.0 e successive), i dati pixel delle bitmap vengono archiviati nell'heap nativo, mentre solo un piccolo oggetto wrapper risiede nell'heap Java.
Quando un'app deve presentare un'immagine, di solito viene decodificata da un file immagine compresso in una bitmap e archiviata nell'heap.
Bitmap condivise (ashmem/memfd)
Quando una bitmap viene trasferita tra i processi (ad es. tramite Binder a SystemUI per una notifica), Android evita di copiare i dati pixel utilizzando la memoria condivisa (ashmem o memfd).
Un'istanza Bitmap può essere copiata esplicitamente nella memoria condivisa chiamando
Bitmap.asShared(),
o implicitamente se un Bitmap viene inserito in un Parcel (in genere aggiungendo la
bitmap a un Parcelable come un Bundle) e inviato tramite Binder IPC.
Quando una bitmap condivisa viene inviata tramite Binder IPC, i dati pixel stessi non vengono copiati, ma un descrittore del file che fa riferimento a una regione di memoria condivisa viene duplicato nel processo destinatario. La regione di memoria sottostante può essere condivisa tra più processi e non viene liberata finché non vengono chiusi tutti i descrittori di file che fanno riferimento a essa.
Bitmap modificabili e immutabili
- Bitmap modificabili: possono essere modificate dopo la creazione (ad es. tramite un
Canvas). Richiedono sempre la propria allocazione di memoria privata. Se viene copiata una bitmap modificabile, è necessario creare una copia completa (seconda copia di tutti i dati pixel). - Bitmap immutabili: non possono essere modificate. Ciò consente ottimizzazioni come la condivisione dello stesso buffer di memoria sottostante tra diverse istanze
Bitmap. Le bitmap caricate dalle risorse APK (BitmapFactory) sono in genere immutabili.
Gestione efficiente delle bitmap
Pooling e riutilizzo delle bitmap
L'allocazione e la deallocazione frequenti delle bitmap causano un'oscillazione dell'allocazione, che costringe il GC a essere eseguito costantemente. Le librerie di caricamento delle immagini comuni utilizzano un pool di bitmap.
Google consiglia Glide come soluzione per le applicazioni basate su Java e Coil per le applicazioni basate su Kotlin (soprattutto quando si utilizza Jetpack Compose).
Quando una bitmap non è più necessaria, anziché lasciarla raccogliere dal GC, l'app chiama bitmap.recycle() o la restituisce a un pool. La volta successiva che è necessaria una bitmap con le stesse dimensioni e la stessa configurazione, il pool fornisce il buffer esistente, evitando una nuova allocazione.
Bitmap hardware
Bitmap.Config.HARDWARE consente di archiviare i dati pixel direttamente nella memoria video (DMABuf).
- Vantaggi:
- Risparmio di memoria: non utilizza l'heap nativo o dell'applicazione, ma la memoria della GPU. Spesso le bitmap mostrate nell'interfaccia utente di un'app devono essere comunque copiate nella memoria della GPU, quindi questa operazione di copia e il costo di memoria aggiuntivo vengono risparmiati.
- Prestazioni: il disegno è estremamente veloce perché i dati sono già su lla GPU.
- Svantaggi:
- Immutabili: le bitmap hardware non possono essere modificate.
- La lettura è lenta: l'accesso ai pixel dalla CPU (ad es.
getPixel()) è molto costoso. - Attribuzione: più difficile da monitorare negli strumenti standard come AHAT (vedi sotto).
Esercitazione pratica: esplorazione delle bitmap
Utilizzeremo l'app di esempio BitmapLab per esplorare questi concetti.
1. Misurazione con dumpsys meminfo
Avvia BitmapLab e tocca ALLOCATE 10MB ARGB_8888. Dopodiché, esegui:
adb shell dumpsys meminfo -s com.android.bitmaplab
Nelle versioni moderne di Android, cerca la sezione Native Allocations. Queste forniscono un'attribuzione molto migliore per le bitmap rispetto al riepilogo dell'app generico:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- Bitmap (malloced): bitmap allocate nell'heap nativo del processo. È qui che risiedono la maggior parte delle bitmap standard in Android 8.0 e versioni successive.
- Bitmap (nonmalloced): bitmap che utilizzano memoria specializzata come
bitmap hardware o bitmap condivise (tramite
ashmemomemfd).
Se allochi una bitmap condivisa in BitmapLab, la vedrai riflessa
in Bitmap (nonmalloced):
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240
Bitmap (nonmalloced): 1 10240 # <--- Shared Bitmap!
Monitoraggio delle bitmap condivise
In alcune versioni di Android e configurazioni del kernel, dumpsys meminfo fornisce anche un monitoraggio ad alta risoluzione delle bitmap mappate nello spazio degli indirizzi del processo tramite descrittori di file.
Per impostazione predefinita, le bitmap condivise utilizzano un nome generico ("bitmap"). Per attivare l'attribuzione dettagliata e il monitoraggio univoco delle bitmap (identificando le bitmap condivise in processi diversi), devi attivare la seguente proprietà di sistema:
adb shell setprop debug.hwui.bitmap_ashmem_long_name true
Se attivata, le regioni ashmem in /proc/<pid>/smaps avranno nomi più
descrittivi. meminfo ne trarrà vantaggio e i risultati saranno simili a questi:
Shared Bitmaps
Count Size(KB)
------ ------
Mapped: 1 10240
Unique: 1 10240
- Mapped: le dimensioni totali di tutti i mapping di memoria correlati alle bitmap.
- Unico: le dimensioni delle bitmap considerando solo quelle uniche (ovvero due o più mapping degli stessi dati pixel della bitmap condivisa sottostante vengono conteggiati una sola volta).
2. Bitmap in AHAT
AHAT fornisce un'eccellente visualizzazione per le bitmap.
- In BitmapLab, alloca alcune bitmap.
Acquisisci un dump dell'heap con il flag
-b(per includere i dati bitmap nativi):adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof adb pull /data/local/tmp/bitmaps.hprof . ahat bitmaps.hprofApri
localhost:7100e cerca il link Bitmap nella barra laterale o la classeBitmap.AHAT eseguirà il rendering delle bitmap nel browser, semplificando l'identificazione delle immagini che occupano più memoria.

3. Tracce bitmap in Perfetto
Perfetto può monitorare le allocazioni e i conteggi delle bitmap nel tempo. Questi contatori vengono emessi dal framework Android quando la categoria atrace gfx è attivata per un'applicazione specifica.
Avvia una traccia. Devi includere la categoria
gfxe scegliere come target il pacchetto applicativo specifico utilizzando il flag-a:external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \ -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplabIn BitmapLab, tocca ripetutamente i pulsanti Allocate e Clear.
Tocca anche Parcel/Unparcel Bitmap.
Analizza la traccia in ui.perfetto.dev.
Nella sezione del processo per com.android.bitmaplab, vedrai:
* Bitmap Count: un contatore che mostra il numero di bitmap attive.
* Memoria bitmap: un contatore che mostra il numero totale di byte utilizzati dalle bitmap.
Slice di alto livello (SDK Perfetto)
BitmapLab utilizza anche l'SDK Perfetto per emettere slice di alto livello per le operazioni bitmap. Cerca BitmapLab_ nella traccia per trovare:
* BitmapLab_parcelUnparcel: slice che coprono la logica di suddivisione e unparceling
logic.
* BitmapLab_postNotification: slice che coprono il flusso di pubblicazione delle notifiche.
Monitoraggio dei flussi di notifica
Quando tocchi Post Notification, l'app crea una notifica contenente la bitmap corrente e la invia al sistema. Il codice del framework responsabile di questa operazione emette slice Perfetto con eventi di flusso che collegano la suddivisione (scrittura della bitmap in un Parcel da inviare tramite Binder IPC) e l'unparceling (lettura della bitmap da un Parcel sul lato ricevente).
Nello screenshot riportato di seguito puoi vedere l'app che suddivide la bitmap di grandi dimensioni da utilizzare in una transazione Binder per pubblicare la notifica e l'unparceling corrispondente nel processo system_server.

Utilizzando Perfetto, puoi persino seguire la stessa bitmap di notifica mentre si propaga ulteriormente tra thread e processi, ad esempio da un thread Binder in system_server (che implementa il server Binder INotificationManager) ai thread di lavoro system_server che potrebbero quindi inoltrare la stessa bitmap a com.android.systemui per essere visualizzata nella barra delle notifiche.
Sfide delle app di sistema
Le app di sistema come SystemUI (Notifiche) e Launcher presentano sfide uniche:
- Contenuti illimitati: le notifiche e i widget possono essere numerosi. Se ognuno contiene una bitmap di grandi dimensioni, il sistema può esaurire rapidamente la memoria.
- Duplicazione: la stessa icona dell'app potrebbe essere presente nella cache di Launcher, nell'area di notifica di SystemUI e nell'app Impostazioni.
- Condivisione tramite buffer hardware: per mitigare questo problema, i componenti di sistema si stanno
spostando verso un servizio centralizzato di "scaricamento delle immagini" che condivide le istanze
HardwareBuffertra i processi. Attribuzione DMABuf: le bitmap hardware risparmiano spazio nell'heap, ma utilizzano la memoria DMABuf , che è più difficile da attribuire a un processo specifico negli strumenti di memoria standard.
Utilizza
adb shell dmabuf_dumpper visualizzare le allocazioni DMABuf a livello di sistema. Questo strumento fornisce una suddivisione dei buffer per processo: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: le dimensioni totali del buffer se è mappato nel processo.
- Pss: le dimensioni proporzionali (RSS diviso per il numero di processi che condividono il buffer). Questa è la metrica migliore per la contabilità.
- nr_procs: il numero di processi che attualmente contengono un riferimento a questo buffer.
- Exporter: il driver che ha allocato il buffer (ad es.
virtio_gpusu Cuttlefish o un heap Ion/DMA-BUF specifico del fornitore sull'hardware).
Puoi anche utilizzare
adb shell dmabuf_dump -bper un riepilogo di tutti i buffer e dell'utilizzo totale di DMA-BUF a livello di sistema.