Bitmap dan memori

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 ashmem atau memfd).

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.

  1. Di BitmapLab, alokasikan beberapa bitmap.
  2. 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.hprof
    
  3. Buka localhost:7100 dan cari link Bitmap di sidebar atau cari class Bitmap.

  4. AHAT akan merender bitmap di browser, sehingga memudahkan Anda mengidentifikasi gambar mana yang menghabiskan banyak memori.

AHAT menampilkan bitmap yang dirender

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.

  1. Mulai rekaman aktivitas. Anda harus menyertakan kategori gfx dan 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.bitmaplab
    
  2. Di BitmapLab, ketuk tombol Allocate dan Clear berulang kali.

  3. Ketuk juga Parcel/Unparcel Bitmap.

  4. 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.

Perfetto menampilkan alur dari BitmapLab ke system_server melalui Notifikasi

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:

  1. Konten Tanpa Batas: Notifikasi dan Widget bisa sangat banyak. Jika setiap objek menyimpan bitmap besar, sistem dapat dengan cepat kehabisan memori.
  2. Duplikasi: Ikon aplikasi yang sama mungkin disimpan di cache Peluncur, area notifikasi SystemUI, dan aplikasi Setelan.
  3. Berbagi melalui Buffer Hardware: Untuk mengurangi masalah ini, komponen sistem beralih ke layanan "penurunan beban gambar" terpusat yang membagikan instance HardwareBuffer di seluruh proses.
  4. 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_dump untuk 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_gpu di Cuttlefish, atau heap Ion/DMA-BUF khusus vendor di hardware).

    Anda juga dapat menggunakan adb shell dmabuf_dump -b untuk ringkasan semua buffer dan total penggunaan DMA-BUF di seluruh sistem.


← Java | ↑ Naik | Native →