Objek bitmap sering kali menjadi kontributor tunggal terbesar untuk jejak memori aplikasi. Baik itu ikon aplikasi, gambar notifikasi, atau konten media, penanganan bitmap yang tidak efisien dapat dengan cepat menyebabkan error Kehabisan Memori (OOM) dan tekanan memori di seluruh sistem.
Konfigurasi bitmap dan data piksel
Jumlah memori yang digunakan bitmap terutama ditentukan oleh dimensinya (lebar × tinggi) dan konfigurasinya (Bitmap.Config).
Konfigurasi menentukan jumlah byte yang digunakan untuk merepresentasikan setiap piksel:
| Konfigurasi | Byte per Piksel | Deskripsi |
|---|---|---|
ALPHA_8 |
1 | Hanya saluran alfa (transparansi). Berguna untuk masker. |
RGB_565 |
2 | Merah (5 bit), Hijau (6 bit), Biru (5 bit). Tidak ada alfa. Cocok untuk gambar buram yang tidak memerlukan akurasi warna tinggi. |
ARGB_8888 |
4 | Alpha, Merah, Hijau, Biru (masing-masing 8 bit). Default dan paling umum. |
RGBA_F16 |
8 | Floating point presisi setengah. Digunakan untuk konten HDR dan gamut lebar. |
HARDWARE |
T/A | Disimpan dalam memori grafis (gralloc/DMABuf). Lihat Bitmap Hardware. |
Rumus Memori: Memory (Bytes) = Width × Height × Bytes Per Pixel
Misalnya, gambar layar penuh di perangkat 1080p (1920x1080) dalam ARGB_8888
membutuhkan: 1920 × 1080 × 4 byte ≈ 8,3 MB.
Bitmap heap vs. bitmap bersama
Bitmap heap (heap native)
Di Android modern (8.0+), data piksel bitmap disimpan di Native Heap, sementara hanya objek wrapper kecil yang berada di heap Java.
Saat aplikasi perlu menampilkan gambar, gambar tersebut biasanya didekode dari file gambar terkompresi ke dalam Bitmap dan disimpan di heap.
Bitmap bersama (ashmem/memfd)
Saat bitmap ditransfer antar-proses (misalnya, melalui Binder ke SystemUI untuk notifikasi), Android menghindari penyalinan data piksel dengan menggunakan memori bersama (ashmem atau memfd).
Instance Bitmap dapat disalin ke memori bersama secara eksplisit dengan memanggil
Bitmap.asShared(),
atau secara implisit jika Bitmap dimasukkan ke dalam Parcel (biasanya dengan menambahkan
Bitmap ke Parcelable seperti Bundle) dan dikirim melalui Binder IPC.
Saat Bitmap bersama dikirim melalui Binder IPC, data piksel itu sendiri tidak disalin, tetapi deskriptor file yang mereferensikan region memori bersama diduplikasi ke proses penerima. Wilayah memori yang mendasarinya dapat dibagikan di antara beberapa proses, dan tidak dibebaskan hingga semua deskriptor file yang mereferensikannya ditutup.
Bitmap yang dapat diubah vs. bitmap yang tidak dapat diubah
- Bitmap yang Dapat Diubah: Dapat diubah setelah dibuat (misalnya, melalui
Canvas). Bitmap ini selalu memerlukan alokasi memori pribadi sendiri. Jika Bitmap yang dapat diubah disalin, salinan dalam (salinan kedua dari semua data piksel) harus dibuat. - Bitmap Tidak Dapat Diubah: Tidak dapat diubah. Hal ini memungkinkan pengoptimalan seperti
berbagi buffer memori pokok yang sama di antara berbagai instance
Bitmap. Bitmap yang dimuat dari resource APK (BitmapFactory) biasanya tidak dapat diubah.
Penanganan bitmap yang efisien
Penggabungan dan penggunaan kembali bitmap
Mengalokasikan dan membatalkan alokasi bitmap secara sering menyebabkan churn alokasi, yang memaksa GC berjalan terus-menerus. Library pemuatan gambar umum menggunakan Kumpulan Bitmap.
Google merekomendasikan Glide sebagai solusi untuk aplikasi berbasis Java, dan Coil untuk aplikasi berbasis Kotlin (terutama saat menggunakan Jetpack Compose).
Jika tidak lagi diperlukan, bukan membiarkannya di-GC, aplikasi akan memanggil
bitmap.recycle() atau mengembalikannya ke kumpulan. Saat berikutnya bitmap dengan dimensi dan konfigurasi yang sama diperlukan, kumpulan menyediakan buffer yang ada, sehingga tidak perlu alokasi baru.
Bitmap hardware
Bitmap.Config.HARDWARE memungkinkan Anda menyimpan data piksel langsung di memori
grafis (DMABuf).
- Kelebihan:
- Penghematan Memori: Tidak menggunakan heap aplikasi atau native; menggunakan memori GPU. Sering kali, Bitmap yang ditampilkan di UI aplikasi perlu disalin ke memori GPU, jadi hal ini menghemat operasi penyalinan tersebut dan biaya memori tambahan.
- Performa: Sangat cepat digambar karena data sudah ada di GPU.
- Kekurangan:
- Tidak dapat diubah: Bitmap hardware tidak dapat diubah.
- Baca kembali lambat: Mengakses piksel dari CPU (misalnya,
getPixel()) sangat mahal. - Atribusi: Lebih sulit dilacak di alat standar seperti AHAT (lihat di bawah).
Kesalahan umum memori bitmap
Meskipun Anda menggunakan konfigurasi bitmap modern, beberapa pola berulang dalam cara Anda mendekode dan menjadwalkan bitmap dapat menyebabkan lonjakan memori yang besar.
Mendekode bitmap yang terlalu besar
Foto beresolusi penuh 4000 × 3000 piksel berukuran 48 MB dalam ARGB_8888. Mendekode
seluruh gambar hanya untuk merendernya di dalam thumbnail 200 × 150 piksel akan membuang
lebih dari 99% buffer piksel yang dialokasikan.
Saat Anda mendekode gambar secara langsung dengan ImageDecoder atau
BitmapFactory, lakukan penurunan sampel selama proses dekode agar sesuai dengan
dimensi tampilan target menggunakan ImageDecoder.setTargetSize() atau
BitmapFactory.Options.inSampleSize. Library pemuatan gambar seperti Glide dan Coil melakukan penurunan sampel ini secara otomatis saat Anda memberikan ukuran tampilan target yang dibatasi.
Misalnya, saat mendekode bitmap dengan ImageDecoder, teruskan
OnHeaderDecodedListener yang menskalakan
dimensi output ke ukuran tampilan target Anda:
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)
}
}
Retensi serentak yang tinggi dari decoding paralel
Meskipun setiap bitmap berukuran tepat dan berumur pendek, mendekode banyak gambar secara paralel dapat menyebabkan lonjakan memori yang parah. Misalnya, jika layar galeri atau penyelenggara mengirimkan 30 tugas pada thread pool yang tidak terikat untuk mendekode ikon atau thumbnail secara bersamaan, semua buffer piksel yang tidak dikompresi dan buffer sementara dekoder akan menempati RAM pada saat yang sama.
Retensi serentak yang tinggi ini meningkatkan jejak memori native heap puncak dan dapat memicu penghentian lmkd sebelum batch selesai. Batasi konkurensi dekode Anda
dengan thread pool, semaphore, atau dispatcher coroutine terbatas seperti
Dispatchers.IO.limitedParallelism(2) sehingga hanya beberapa bitmap yang didekode dalam proses
sekaligus.
Bitmap sementara yang tidak didaur ulang dalam loop pemrosesan frame
Di Android 8.0 dan yang lebih tinggi, objek wrapper Bitmap Java hanya berukuran sekitar 56
byte di heap Java, sementara buffer pikselnya berada di heap native dan dapat
berukuran beberapa megabyte. Anda dapat memverifikasi pemisahan ini di Memory Profiler Android Studio atau AHAT, dengan setiap instance Bitmap menampilkan ukuran Java dangkal ~56 byte bersama dengan ukuran native multi-megabyte, dan di dumpsys meminfo di bagian Native Allocations
(Bitmap (malloced)).
Pipeline frekuensi tinggi seperti analisis frame kamera, OCR, atau loop inferensi ML sering mengalokasikan bitmap baru di setiap frame dengan memanggil ImageProxy.toBitmap() dan Bitmap.createBitmap() untuk rotasi atau pemangkasan.
Menghapus referensi ke frame yang digantikan tanpa mendaur ulangnya dapat meningkatkan penggunaan memori native. Wrapper Java kecil hampir tidak meningkatkan penggunaan heap Java, sehingga tidak memicu pembersihan sampah memori cukup cepat untuk mencegah ratusan megabyte buffer piksel native terakumulasi sebelum NativeAllocationRegistry mereklamasi buffer tersebut.
Saat memproses frame dalam loop yang ketat, gunakan kembali buffer yang telah dialokasikan sebelumnya jika memungkinkan, atau panggil bitmap.recycle() secara eksplisit pada bitmap perantara sementara segera setelah setiap frame selesai diproses.
Latihan langsung: eksplorasi bitmap
Kita akan menggunakan aplikasi contoh BitmapLab untuk mempelajari konsep ini.
1. Mengukur dengan dumpsys meminfo
Luncurkan BitmapLab, lalu ketuk ALLOCATE 10MB ARGB_8888. Kemudian, jalankan:
adb shell dumpsys meminfo -s com.android.bitmaplab
Pada versi Android modern, cari bagian Alokasi Native. Hal ini memberikan atribusi yang jauh lebih baik untuk bitmap daripada Ringkasan Aplikasi umum:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- Bitmap (dialokasikan): Bitmap yang dialokasikan di heap native proses. Di sinilah sebagian besar bitmap standar berada di Android 8.0+.
- Bitmap (nonmalloced): Bitmap yang menggunakan memori khusus seperti
Bitmap Hardware atau Bitmap Bersama (melalui
ashmemataumemfd).
Jika Anda mengalokasikan Bitmap Bersama di BitmapLab, Anda akan melihatnya tercermin
di Bitmap (nonmalloced):
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240
Bitmap (nonmalloced): 1 10240 # <--- Shared Bitmap!
Pelacakan bitmap bersama
Pada beberapa versi Android dan konfigurasi kernel, dumpsys meminfo juga
menyediakan pelacakan resolusi tinggi untuk bitmap yang dipetakan ke ruang alamat
proses melalui deskriptor file.
Secara default, bitmap bersama menggunakan nama generik ("bitmap"). Untuk mengaktifkan atribusi mendetail dan pelacakan bitmap unik (mengidentifikasi bitmap bersama di berbagai proses), Anda harus mengaktifkan properti sistem berikut:
adb shell setprop debug.hwui.bitmap_ashmem_long_name true
Jika diaktifkan, region ashmem di /proc/<pid>/smaps akan memiliki nama yang lebih deskriptif. meminfo akan memanfaatkan hal tersebut, dan hasilnya akan terlihat seperti ini:
Shared Bitmaps
Count Size(KB)
------ ------
Mapped: 1 10240
Unique: 1 10240
- Dipetakan: Ukuran total semua pemetaan memori terkait bitmap.
- Unik: Ukuran bitmap hanya mempertimbangkan yang unik (yaitu dua atau lebih pemetaan data piksel Bitmap bersama yang sama dihitung hanya sekali).
2. Bitmap di AHAT
AHAT memberikan visualisasi yang sangat baik untuk Bitmap.
- Di BitmapLab, alokasikan beberapa bitmap.
Ambil heap dump dengan flag
-b(untuk menyertakan data bitmap native):adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof adb pull /data/local/tmp/bitmaps.hprof . ahat bitmaps.hprofBuka
localhost:7100dan cari link Bitmap di sidebar atau cari classBitmap.AHAT akan merender bitmap di browser, sehingga memudahkan Anda mengidentifikasi gambar mana yang menghabiskan banyak memori.

3. Trek bitmap di Perfetto
Perfetto dapat melacak alokasi dan jumlah bitmap dari waktu ke waktu. Penghitung ini dikeluarkan oleh framework Android saat kategori atrace gfx diaktifkan untuk aplikasi tertentu.
Mulai rekaman aktivitas. Anda harus menyertakan kategori
gfxdan menargetkan paket aplikasi tertentu menggunakan 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.bitmaplabDi BitmapLab, ketuk tombol Allocate dan Clear berulang kali.
Ketuk juga Parcel/Unparcel Bitmap.
Analisis rekaman aktivitas di ui.perfetto.dev.
Di bagian proses untuk com.android.bitmaplab, Anda akan melihat:
* Jumlah Bitmap: Penghitung yang menampilkan jumlah bitmap aktif.
* Memori Bitmap: Penghitung yang menampilkan total byte yang digunakan oleh bitmap.
Slice tingkat tinggi (Perfetto SDK)
BitmapLab juga menggunakan Perfetto SDK untuk memancarkan slice tingkat tinggi untuk
operasi bitmap. Telusuri BitmapLab_ di rekaman aktivitas untuk menemukan:
* BitmapLab_parcelUnparcel: Slice yang mencakup logika
pemaketan dan pembatalan pemaketan.
* BitmapLab_postNotification: Slice yang mencakup alur postingan notifikasi.
Melacak alur notifikasi
Saat Anda mengetuk Posting Notifikasi, aplikasi akan membuat notifikasi yang berisi bitmap saat ini dan mengirimkannya ke sistem. Kode framework yang bertanggung jawab untuk ini memancarkan slice Perfetto dengan peristiwa alur yang menghubungkan pengemasan (menulis bitmap ke Parcel untuk dikirim melalui Binder IPC) dan pembongkaran (membaca bitmap dari Parcel di sisi penerima).
Pada screenshot di bawah, Anda dapat melihat aplikasi yang memaketkan bitmap besar untuk
digunakan dalam transaksi Binder untuk memposting notifikasi, dan pembatalan
paket yang sesuai dalam proses system_server.

Dengan Perfetto, Anda bahkan dapat mengikuti bitmap notifikasi yang sama saat bitmap tersebut dipropagasi lebih lanjut di seluruh thread dan proses, misalnya dari thread binder di system_server (yang mengimplementasikan server Binder INotificationManager) ke thread pekerja system_server yang kemudian dapat meneruskan bitmap yang sama ke com.android.systemui untuk ditampilkan di panel notifikasi.
Tantangan aplikasi sistem
Aplikasi sistem seperti SystemUI (Notifikasi) dan Peluncur menghadapi tantangan unik:
- Konten Tanpa Batas: Notifikasi dan Widget bisa sangat banyak. Jika setiap objek menyimpan bitmap besar, sistem dapat dengan cepat kehabisan memori.
- Duplikasi: Ikon aplikasi yang sama mungkin disimpan di cache Peluncur, area notifikasi SystemUI, dan aplikasi Setelan.
- Berbagi melalui Buffer Hardware: Untuk mengurangi masalah ini, komponen sistem beralih ke layanan "penurunan beban gambar" terpusat yang membagikan instance
HardwareBufferdi seluruh proses. Atribusi DMABuf: Bitmap hardware menghemat ruang heap, tetapi menggunakan memori DMABuf, yang lebih sulit diatribusikan ke proses tertentu dalam alat memori standar.
Gunakan
adb shell dmabuf_dumpuntuk melihat alokasi DMABuf di seluruh sistem. Alat ini memberikan perincian buffer per proses: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: Total ukuran buffer jika dipetakan dalam proses.
- Pss: Ukuran proporsional (RSS dibagi dengan jumlah proses yang berbagi buffer). Ini adalah metrik terbaik untuk akuntansi.
- nr_procs: Jumlah proses yang saat ini memegang referensi ke buffer ini.
- Exporter: Driver yang mengalokasikan buffer (misalnya,
virtio_gpudi Cuttlefish, atau heap Ion/DMA-BUF khusus vendor di hardware).
Anda juga dapat menggunakan
adb shell dmabuf_dump -buntuk ringkasan semua buffer dan total penggunaan DMA-BUF di seluruh sistem.