Gli oggetti bitmap sono spesso i maggiori contributori singoli al footprint della memoria di un'applicazione. Che si tratti di icone di 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 dei 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 alpha. Ideale per immagini opache in cui la fedeltà dei colori non è fondamentale. |
ARGB_8888 |
4 | Alpha, Rosso, Verde, Blu (8 bit ciascuno). Opzione predefinita e più comune. |
RGBA_F16 |
8 | Virgola mobile a mezza precisione. Utilizzato per contenuti HDR e ad ampia gamma. |
HARDWARE |
N/D | Archiviati 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 a 1080p (1920 x 1080) in ARGB_8888
richiede: 1920 × 1080 × 4 byte ≈ 8,3 MB.
Bitmap dell'heap e bitmap condivise
Bitmap heap (heap nativo)
In Android moderno (8.0 e versioni successive), i dati dei pixel bitmap vengono archiviati nell'heap nativo, mentre solo un piccolo oggetto wrapper risiede nell'heap Java.
Quando un'app deve presentare un'immagine, questa viene solitamente decodificata da un file immagine compresso in una bitmap e archiviata nell'heap.
Bitmap condivisi (ashmem/memfd)
Quando una bitmap viene trasferita tra processi (ad es. tramite Binder a SystemUI
per una notifica), Android evita di copiare i dati dei pixel utilizzando la memoria
condivisa (ashmem o memfd).
Un'istanza Bitmap può essere copiata nella memoria condivisa in modo esplicito chiamando
Bitmap.asShared(),
o in modo implicito 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 dei pixel non vengono copiati, ma viene duplicato nel processo destinatario un descrittore del file che fa riferimento a una regione di memoria condivisa. 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 vi fanno riferimento.
Bitmap modificabili e immutabili
- Bitmap modificabili: possono essere modificati dopo la creazione (ad es. tramite un
Canvas). Richiedono sempre la propria allocazione di memoria privata. Se viene copiata una bitmap modificabile, deve essere creata una copia completa (seconda copia di tutti i dati dei pixel). - Bitmap immutabili: non possono essere modificati. Ciò consente ottimizzazioni come
la condivisione dello stesso buffer di memoria sottostante tra diverse istanze di
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 di bitmap causano un churn di 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 utilizzi Jetpack Compose).
Quando una bitmap non è più necessaria, anziché lasciarla raccogliere dalla Garbage Collection, l'app chiama
bitmap.recycle() o la restituisce a un pool. La volta successiva che è necessario un bitmap con le stesse
dimensioni e configurazione, il pool fornisce il buffer esistente,
evitando una nuova allocazione.
Bitmap hardware
Bitmap.Config.HARDWARE ti consente di archiviare i dati dei pixel direttamente nella memoria
grafica (DMABuf).
- Vantaggi:
- Risparmio di memoria: non utilizza l'heap nativo o dell'applicazione; utilizza la memoria della GPU. Spesso le bitmap mostrate nell'interfaccia utente di un'app devono comunque essere copiate nella memoria della GPU, quindi questa operazione di copia e il costo aggiuntivo della memoria vengono risparmiati.
- Rendimento: estremamente veloce da disegnare perché i dati sono già sulla GPU.
- Svantaggi:
- Immutabile: le bitmap hardware non possono essere modificate.
- Read-back è lento: l'accesso ai pixel dalla CPU (ad es.
getPixel()) è molto costoso. - Attribuzione: più difficile da monitorare negli strumenti standard come AHAT (vedi di seguito).
Errori comuni relativi alla memoria bitmap
Anche quando utilizzi configurazioni bitmap moderne, diversi pattern ricorrenti nel modo in cui decodifichi e pianifichi i bitmap possono causare picchi di memoria elevati.
Decodifica di bitmap in overscaling
Una foto a risoluzione completa di 4000 × 3000 pixel occupa 48 MB in ARGB_8888. La decodifica
dell'intera immagine solo per visualizzarla all'interno di una miniatura di 200 × 150 pixel spreca
oltre il 99% del buffer di pixel allocato.
Quando decodifichi le immagini direttamente con ImageDecoder o
BitmapFactory, esegui il downsampling durante il passaggio di decodifica in modo che corrispondano
alle dimensioni della visualizzazione di destinazione utilizzando ImageDecoder.setTargetSize() o
BitmapFactory.Options.inSampleSize. Librerie di caricamento delle immagini come Glide e
Coil eseguono automaticamente questo downsampling quando fornisci una dimensione della visualizzazione di destinazione
delimitata.
Ad esempio, quando decodifichi una bitmap con ImageDecoder, passa
un OnHeaderDecodedListener che ridimensiona
le dimensioni di output in base alle dimensioni della visualizzazione di destinazione:
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)
}
}
Tasso di fidelizzazione simultanea elevato dalla decodifica parallela
Anche quando le singole bitmap sono dimensionate correttamente e di breve durata, la decodifica di molte immagini in parallelo può causare picchi di memoria elevati. Ad esempio, se una schermata dell'organizzatore o della galleria invia 30 attività a un pool di thread illimitato per decodificare icone o miniature contemporaneamente, tutti i 30 buffer di pixel non compressi e i buffer temporanei del decodificatore occupano la RAM contemporaneamente.
Questa conservazione simultanea elevata aumenta l'utilizzo dell'heap nativo di picco e può
attivare lmkd interruzioni prima del completamento del batch. Limita la concorrenza di decodifica
con un pool di thread, un semaforo o un dispatcher di coroutine limitato, ad esempio
Dispatchers.IO.limitedParallelism(2), in modo che solo poche bitmap vengano decodificate contemporaneamente.
Bitmap temporanei non riciclati nei loop di elaborazione dei frame
In Android 8.0 e versioni successive, l'oggetto wrapper Java Bitmap occupa solo circa 56
byte nell'heap Java, mentre il buffer dei pixel si trova nell'heap nativo e può
occupare diversi megabyte. Puoi verificare questa suddivisione in Android Studio Memory Profiler o AHAT, dove ogni istanza Bitmap mostra una dimensione Java superficiale di circa 56 byte insieme alla sua dimensione nativa di più megabyte e in dumpsys meminfo in Native Allocations (Bitmap (malloced)).
Le pipeline ad alta frequenza, come l'analisi dei frame della videocamera, l'OCR o i loop di inferenza ML, spesso allocano una nuova bitmap su ogni frame chiamando ImageProxy.toBitmap() e Bitmap.createBitmap() per la rotazione o il ritaglio.
L'eliminazione dei riferimenti ai frame sostituiti senza riciclarli può aumentare
la memoria nativa. I piccoli wrapper Java aumentano di poco l'occupazione dell'heap Java, quindi
non attivano la garbage collection abbastanza rapidamente da impedire l'accumulo di centinaia di
megabyte di buffer di pixel nativi prima che
NativeAllocationRegistry li recuperi.
Quando elabori i frame in un ciclo stretto, riutilizza i buffer preallocati, se
possibile, o chiama bitmap.recycle() in modo esplicito su bitmap intermedi
temporanei non appena l'elaborazione di ogni frame è terminata.
Esercizio pratico: esplorazione 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 Allocazioni native. Questi forniscono un'attribuzione molto migliore per le bitmap rispetto al Riepilogo app generico:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- Bitmap (allocato): bitmap allocati nell'heap nativo del processo. È qui che si trovano la maggior parte delle bitmap standard in Android 8.0 e versioni successive.
- Bitmap (non malloced): 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
il monitoraggio ad alta risoluzione per le bitmap mappate nello spazio
di indirizzi del processo tramite i descrittori di file.
Per impostazione predefinita, le bitmap condivise utilizzano un nome generico ("bitmap"). Per attivare l'attribuzione dettagliata e il monitoraggio bitmap univoco (identificazione di bitmap condivisi in processi diversi), devi attivare la seguente proprietà di sistema:
adb shell setprop debug.hwui.bitmap_ashmem_long_name true
Se abilitata, 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
- Mappato: la dimensione totale di tutte le mappature di memoria correlate alle bitmap.
- Unico: le dimensioni delle bitmap che considerano solo i valori unici (ovvero due o più mappature degli stessi dati pixel bitmap condivisi vengono conteggiate solo una volta).
2. Bitmap in AHAT
AHAT offre un'eccellente visualizzazione per le bitmap.
- In BitmapLab, alloca alcuni 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 cerca la classeBitmap.AHAT esegue 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 di 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 Alloca e Cancella.
Tocca anche Parcel/Unparcel Bitmap.
Analizza la traccia in ui.perfetto.dev.
Nella sezione del processo per com.android.bitmaplab, vedrai:
* Conteggio bitmap: un contatore che mostra il numero di bitmap attive.
* Memoria bitmap: un contatore che mostra il totale dei byte utilizzati dalle bitmap.
Sezioni di alto livello (SDK Perfetto)
BitmapLab utilizza anche l'SDK Perfetto per emettere sezioni di alto livello per le operazioni bitmap. Cerca BitmapLab_ nella traccia per trovare:
* BitmapLab_parcelUnparcel: sezioni che coprono la logica di suddivisione e unione dei pacchetti.
* BitmapLab_postNotification: sezioni che coprono il flusso di pubblicazione delle notifiche.
Monitoraggio dei flussi di notifica
Quando tocchi Notifica post, l'app crea una notifica contenente la bitmap corrente e la invia al sistema. Il codice del framework responsabile di questo emette sezioni 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 all'estremità ricevente).
Nello screenshot seguente puoi vedere l'app che suddivide la bitmap di grandi dimensioni da utilizzare in una transazione Binder per pubblicare la notifica e la corrispondente suddivisione 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 worker system_server che potrebbero quindi inoltrare la stessa bitmap a com.android.systemui per essere visualizzata nell'area notifiche.
Sfide relative alle app di sistema
Le app di sistema come SystemUI (Notifiche) e Launcher presentano sfide uniche:
- Contenuti senza limiti: le notifiche e i widget possono essere numerosi. Se ognuna contiene una bitmap di grandi dimensioni, il sistema può esaurire rapidamente la memoria.
- Duplicazione: la stessa icona dell'app potrebbe essere memorizzata nella cache di Avvio app, nell'area di notifica di SystemUI e nell'app Impostazioni.
- Condivisione tramite buffer hardware: per risolvere 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 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: la dimensione totale del buffer se è mappato nel processo.
- Pss: la dimensione proporzionale (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.
- Esportatore: 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.