Bitmap dan memori

Objek bitmap sering kali menjadi kontributor tunggal terbesar terhadap jejak memori aplikasi. Baik ikon aplikasi, gambar notifikasi, maupun 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 dimensi (lebar × tinggi) dan konfigurasi (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 mask.
RGB_565 2 Merah (5 bit), Hijau (6 bit), Biru (5 bit). Tidak ada alfa. Baik untuk gambar buram yang tidak memerlukan kesetiaan warna tinggi.
ARGB_8888 4 Alfa, 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 memerlukan: 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 Heap Native, sedangkan hanya objek wrapper kecil yang berada di heap Java.

Saat aplikasi perlu menampilkan gambar, gambar biasanya didekode dari file gambar terkompresi ke 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 ditempatkan di 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. Region memori yang mendasarinya dapat dibagikan di antara beberapa proses, dan tidak akan dikosongkan 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. Jika Bitmap yang dapat diubah disalin, salinan mendalam (salinan kedua dari semua data piksel) harus dibuat.
  • Bitmap yang Tidak Dapat Diubah: Tidak dapat diubah. Hal ini memungkinkan pengoptimalan seperti berbagi buffer memori yang sama antara instance Bitmap yang berbeda. Bitmap yang dimuat dari resource APK (BitmapFactory) biasanya tidak dapat diubah.

Penanganan bitmap yang efisien

Pengumpulan dan penggunaan kembali bitmap

Seringnya mengalokasikan dan mendealokasikan bitmap menyebabkan perputaran 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).

Saat bitmap tidak lagi diperlukan, aplikasi akan memanggil bitmap.recycle() atau mengembalikannya ke kumpulan, bukan membiarkannya di-GC. Saat berikutnya bitmap dengan dimensi dan konfigurasi yang sama diperlukan, kumpulan akan menyediakan buffer yang ada, sehingga menghindari alokasi baru.

Bitmap hardware

Bitmap.Config.HARDWARE memungkinkan Anda menyimpan data piksel langsung di memori grafis (DMABuf).

  • Keunggulan:
    • Penghematan Memori: Tidak menggunakan heap aplikasi atau native; menggunakan memori GPU. Sering kali Bitmap yang ditampilkan di UI aplikasi harus disalin ke memori GPU, sehingga hal ini menghemat operasi salin dan biaya memori tambahan.
    • Performa: Sangat cepat digambar karena data sudah ada di GPU.
  • Kekurangan:
    • Tidak Dapat Diubah: Bitmap hardware tidak dapat diubah.
    • Pembacaan ulang lambat: Mengakses piksel dari CPU (misalnya, getPixel()) sangat mahal.
    • Atribusi: Lebih sulit dilacak di alat standar seperti AHAT (lihat di bawah).

Latihan praktik: eksplorasi bitmap

Kita akan menggunakan aplikasi contoh BitmapLab untuk mempelajari konsep ini.

1. Mengukur dengan dumpsys meminfo

Luncurkan BitmapLab dan ketuk ALLOCATE 10MB ARGB_8888. Kemudian, jalankan:

adb shell dumpsys meminfo -s com.android.bitmaplab

Pada versi Android modern, cari bagian Native Allocations. Bagian ini memberikan atribusi yang jauh lebih baik untuk bitmap daripada App Summarygenerik:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • Bitmap (malloced): 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 mendasarinya hanya dihitung satu kali).

2. Bitmap di AHAT

AHAT menyediakan visualisasi yang sangat baik untuk Bitmap.

  1. Di BitmapLab, alokasikan beberapa bitmap.
  2. Rekam 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 Bitmaps di sidebar atau cari class Bitmap.

  4. AHAT akan merender bitmap di browser, sehingga memudahkan identifikasi gambar yang menggunakan banyak memori.

AHAT menampilkan bitmap yang dirender

3. Rekaman aktivitas bitmap di Perfetto

Perfetto dapat melacak alokasi dan jumlah bitmap dari waktu ke waktu. Penghitung ini ditampilkan 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. * Bitmap Memory: Penghitung yang menampilkan total byte yang digunakan oleh bitmap.

Slice tingkat tinggi (Perfetto SDK)

BitmapLab juga menggunakan Perfetto SDK untuk menampilkan slice tingkat tinggi untuk operasi bitmap. Cari BitmapLab_ dalam rekaman aktivitas untuk menemukan: * BitmapLab_parcelUnparcel: Slice yang mencakup logika pengemasan dan pembongkaran paket. * BitmapLab_postNotification: Slice yang mencakup alur postingan notifikasi.

Melacak alur notifikasi

Saat Anda mengetuk Post Notification, aplikasi akan membuat notifikasi yang berisi bitmap saat ini dan mengirimkannya ke sistem. Kode framework yang bertanggung jawab untuk hal ini akan menampilkan slice Perfetto dengan peristiwa alur yang menghubungkan pengemasan (menulis bitmap ke Parcel untuk dikirim melalui Binder IPC) dan pembongkaran paket (membaca bitmap dari Parcel di sisi penerima).

Pada screenshot di bawah, Anda dapat melihat aplikasi mengemas bitmap besar untuk digunakan dalam transaksi Binder guna memposting notifikasi, dan pembongkaran paket yang sesuai dalam proses system_server.

Perfetto yang menampilkan alur dari BitmapLab ke system_server melalui Notifikasi

Dengan Perfetto, Anda bahkan dapat mengikuti bitmap notifikasi yang sama saat terus disebarkan di seluruh thread dan proses, misalnya dari thread binder di system_server (yang menerapkan 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 notifikasi dan widget 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 mengatasinya, komponen sistem beralih ke layanan "image offload" terpusat yang berbagi HardwareBuffer instance di seluruh proses.
  4. Atribusi DMABuf: Bitmap hardware menghemat ruang heap, tetapi menggunakan memori DMABuf , yang lebih sulit dikaitkan dengan proses tertentu di 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: Ukuran total 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 menyimpan 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 | ↑ Atas | Native →