Catatan: Untuk sebagian besar kasus, sebaiknya gunakan library Glide untuk mengambil, mendekode, dan menampilkan bitmap dalam aplikasi. Glide menyederhanakan sebagian besar kompleksitas dalam menangani tugas ini dan tugas lain yang terkait dengan penggunaan bitmap dan gambar lain di Android. Untuk informasi cara menggunakan dan mendownload Glide, buka repositori Glide di GitHub.
Memuat satu bitmap ke antarmuka pengguna (UI) sangat mudah, tetapi situasinya akan menjadi lebih
rumit jika Anda harus memuat satu set gambar yang lebih besar sekaligus. Dalam banyak kasus (misalnya pada
komponen seperti ListView
, GridView
, atau ViewPager
), jumlah total gambar di layar jika digabungkan dengan gambar yang
mungkin akan segera di-scroll ke layar, pada dasarnya tidak terbatas.
Penggunaan memori diturunkan dengan komponen seperti ini dengan mendaur ulang tampilan turunan saat berpindah keluar layar. Pembersih sampah memori juga mengosongkan bitmap yang dimuat, dengan asumsi Anda tidak menyimpan referensi jangka lama. Semua ini bagus, tetapi untuk mempertahankan UI yang lancar dan dimuat dengan cepat, sebaiknya hindari memproses gambar ini secara terus-menerus tiap kali gambar ini muncul kembali di layar. Memori dan cache disk sering kali berguna di sini, sehingga komponen dapat dengan cepat memuat ulang gambar yang diproses.
Pelajaran ini akan memandu Anda menggunakan memori dan cache bitmap disk untuk meningkatkan daya respons dan kelancaran UI saat memuat beberapa bitmap.
Menggunakan Cache Memori
Cache memori menawarkan akses yang cepat ke bitmap, tetapi dapat menghabiskan
memori aplikasi yang berharga. Class LruCache
(juga tersedia di Support Library untuk digunakan kembali
ke API Level 4) sangat sesuai dengan tugas menyimpan cache bitmap, dengan mempertahankan
objek yang baru-baru ini direferensikan dalam LinkedHashMap
, yang direferensikan dengan tegas dan mengeluarkan
anggota yang paling lama digunakan sebelum cache melebihi ukuran yang ditentukan.
Catatan: Sebelumnya, implementasi cache memori yang populer digunakan adalah
cache bitmap SoftReference
atau WeakReference
, tetapi
tidak direkomendasikan. Mulai dari Android 2.3 (API Level 9) pembersih sampah memori lebih
agresif mengumpulkan referensi lembut/lemah yang membuatnya agak tidak efektif. Selain itu,
sebelum Android 3.0 (API Level 11), data pendukung bitmap disimpan di memori native yang
tidak dirilis dengan cara yang dapat diprediksi, yang berpotensi menyebabkan aplikasi melewati
batas memorinya dalam waktu singkat dan kemudian error.
Untuk memilih ukuran yang sesuai bagi LruCache
, sejumlah faktor
harus dipertimbangkan, misalnya:
- Seberapa intensif aktivitas dan/atau aplikasi Anda dalam menggunakan memori?
- Berapa banyak gambar yang akan ditampilkan sekaligus di layar? Berapa banyak gambar yang harus tersedia dan siap untuk ditampilkan di layar?
- Berapa ukuran dan kepadatan layar perangkat? Perangkat dengan layar berkepadatan sangat tinggi (xhdpi) seperti Galaxy Nexus memerlukan cache yang lebih besar untuk menampung jumlah gambar yang sama dalam memori dibandingkan dengan perangkat seperti Nexus S (hdpi).
- Apa saja dimensi dan konfigurasi bitmap tersebut dan, oleh karena itu, berapa banyak memori yang akan digunakan?
- Seberapa sering gambar akan diakses? Apakah beberapa gambar yang akan lebih sering diakses daripada yang lain?
Jika demikian, mungkin Anda dapat selalu menyimpan item tertentu dalam memori, atau bahkan memiliki beberapa objek
LruCache
untuk kelompok bitmap yang berbeda. - Dapatkah Anda menyeimbangkan kualitas dengan kuantitas? Terkadang, menyimpan bitmap berkualitas lebih rendah dalam jumlah lebih besar akan lebih baik, yang berpotensi memuat versi kualitas yang lebih tinggi dalam tugas latar belakang lainnya.
Tidak ada ukuran atau formula tertentu yang cocok untuk semua aplikasi, semua terserah pada Anda untuk menganalisis
penggunaan dan membuat solusi yang sesuai. Cache yang terlalu kecil menyebabkan overhead tambahan tanpa
manfaat apa pun, dan cache yang terlalu besar dapat menyebabkan pengecualian java.lang.OutOfMemory
yang membuat aplikasi hanya memiliki sedikit memori untuk digunakan.
Berikut ini contoh cara menyiapkan LruCache
untuk bitmap:
Kotlin
private lateinit var memoryCache: LruCache<String, Bitmap> override fun onCreate(savedInstanceState: Bundle?) { ... // Get max available VM memory, exceeding this amount will throw an // OutOfMemory exception. Stored in kilobytes as LruCache takes an // int in its constructor. val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() // Use 1/8th of the available memory for this memory cache. val cacheSize = maxMemory / 8 memoryCache = object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, bitmap: Bitmap): Int { // The cache size will be measured in kilobytes rather than // number of items. return bitmap.byteCount / 1024 } } ... }
Java
private LruCache<String, Bitmap> memoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... // Get max available VM memory, exceeding this amount will throw an // OutOfMemory exception. Stored in kilobytes as LruCache takes an // int in its constructor. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/8th of the available memory for this memory cache. final int cacheSize = maxMemory / 8; memoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // The cache size will be measured in kilobytes rather than // number of items. return bitmap.getByteCount() / 1024; } }; ... } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { memoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return memoryCache.get(key); }
Catatan: Pada contoh ini, seperdelapan memori aplikasi
dialokasikan untuk cache. Pada perangkat normal/hdpi, jumlah minimum memori yang diperlukan adalah sekitar 4 MB (32/8). Layar
penuh GridView
yang diisi dengan gambar di perangkat dengan resolusi 800 x 480 piksel akan
menggunakan sekitar 1,5 MB (800*480*4 byte), sehingga akan menyimpan cache minimum sekitar 2,5 halaman gambar dalam
memori.
Saat memuat bitmap ke ImageView
, LruCache
akan diperiksa terlebih dahulu. Jika ditemukan, entri tersebut akan segera digunakan untuk mengupdate ImageView
. Jika tidak, thread latar belakang akan digunakan untuk memproses gambar:
Kotlin
fun loadBitmap(resId: Int, imageView: ImageView) { val imageKey: String = resId.toString() val bitmap: Bitmap? = getBitmapFromMemCache(imageKey)?.also { mImageView.setImageBitmap(it) } ?: run { mImageView.setImageResource(R.drawable.image_placeholder) val task = BitmapWorkerTask() task.execute(resId) null } }
Java
public void loadBitmap(int resId, ImageView imageView) { final String imageKey = String.valueOf(resId); final Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { mImageView.setImageBitmap(bitmap); } else { mImageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTask task = new BitmapWorkerTask(mImageView); task.execute(resId); } }
BitmapWorkerTask
juga perlu
diupdate untuk menambahkan entri ke cache memori:
Kotlin
private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() { ... // Decode image in background. override fun doInBackground(vararg params: Int?): Bitmap? { return params[0]?.let { imageId -> decodeSampledBitmapFromResource(resources, imageId, 100, 100)?.also { bitmap -> addBitmapToMemoryCache(imageId.toString(), bitmap) } } } ... }
Java
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } ... }
Menggunakan Cache Disk
Cache memori bermanfaat dalam mempercepat akses ke bitmap yang baru-baru ini dilihat, tetapi Anda tidak dapat
mengandalkan gambar yang tersedia dalam cache ini. Komponen seperti GridView
dengan
set data yang lebih besar dapat memenuhi cache memori dalam waktu singkat. Aplikasi dapat terganggu oleh
tugas lain seperti panggilan telepon, dan saat berada di latar belakang, aplikasi mungkin akan dimatikan dan cache memori
dihancurkan. Setelah pengguna melanjutkan, aplikasi harus memproses kembali setiap gambar.
Cache disk dapat digunakan dalam kasus ini untuk mempertahankan bitmap yang diproses dan membantu mengurangi waktu pemuatan jika gambar tidak tersedia lagi dalam cache memori. Tentu saja, pengambilan gambar dari disk akan lebih lambat daripada pemuatan dari memori dan harus dilakukan di thread latar belakang, karena waktu baca disk tidak dapat diprediksi.
Catatan: ContentProvider
mungkin menjadi
tempat yang lebih cocok untuk menyimpan gambar cache jika lebih sering diakses, misalnya dalam
aplikasi galeri gambar.
Kode contoh dari class ini menggunakan implementasi DiskLruCache
yang diambil dari
sumber Android.
Berikut ini kode contoh yang diupdate yang menambahkan cache disk bersama dengan cache memori yang sudah ada:
Kotlin
private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB private const val DISK_CACHE_SUBDIR = "thumbnails" ... private var diskLruCache: DiskLruCache? = null private val diskCacheLock = ReentrantLock() private val diskCacheLockCondition: Condition = diskCacheLock.newCondition() private var diskCacheStarting = true override fun onCreate(savedInstanceState: Bundle?) { ... // Initialize memory cache ... // Initialize disk cache on background thread val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR) InitDiskCacheTask().execute(cacheDir) ... } internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() { override fun doInBackground(vararg params: File): Void? { diskCacheLock.withLock { val cacheDir = params[0] diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE) diskCacheStarting = false // Finished initialization diskCacheLockCondition.signalAll() // Wake any waiting threads } return null } } internal inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() { ... // Decode image in background. override fun doInBackground(vararg params: Int?): Bitmap? { val imageKey = params[0].toString() // Check disk cache in background thread return getBitmapFromDiskCache(imageKey) ?: // Not found in disk cache decodeSampledBitmapFromResource(resources, params[0], 100, 100) ?.also { // Add final bitmap to caches addBitmapToCache(imageKey, it) } } } fun addBitmapToCache(key: String, bitmap: Bitmap) { // Add to memory cache as before if (getBitmapFromMemCache(key) == null) { memoryCache.put(key, bitmap) } // Also add to disk cache synchronized(diskCacheLock) { diskLruCache?.apply { if (!containsKey(key)) { put(key, bitmap) } } } } fun getBitmapFromDiskCache(key: String): Bitmap? = diskCacheLock.withLock { // Wait while disk cache is started from background thread while (diskCacheStarting) { try { diskCacheLockCondition.await() } catch (e: InterruptedException) { } } return diskLruCache?.get(key) } // Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. fun getDiskCacheDir(context: Context, uniqueName: String): File { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir val cachePath = if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() || !isExternalStorageRemovable()) { context.externalCacheDir.path } else { context.cacheDir.path } return File(cachePath + File.separator + uniqueName) }
Java
private DiskLruCache diskLruCache; private final Object diskCacheLock = new Object(); private boolean diskCacheStarting = true; private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override protected void onCreate(Bundle savedInstanceState) { ... // Initialize memory cache ... // Initialize disk cache on background thread File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR); new InitDiskCacheTask().execute(cacheDir); ... } class InitDiskCacheTask extends AsyncTask<File, Void, Void> { @Override protected Void doInBackground(File... params) { synchronized (diskCacheLock) { File cacheDir = params[0]; diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); diskCacheStarting = false; // Finished initialization diskCacheLock.notifyAll(); // Wake any waiting threads } return null; } } class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { final String imageKey = String.valueOf(params[0]); // Check disk cache in background thread Bitmap bitmap = getBitmapFromDiskCache(imageKey); if (bitmap == null) { // Not found in disk cache // Process as normal final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); } // Add final bitmap to caches addBitmapToCache(imageKey, bitmap); return bitmap; } ... } public void addBitmapToCache(String key, Bitmap bitmap) { // Add to memory cache as before if (getBitmapFromMemCache(key) == null) { memoryCache.put(key, bitmap); } // Also add to disk cache synchronized (diskCacheLock) { if (diskLruCache != null && diskLruCache.get(key) == null) { diskLruCache.put(key, bitmap); } } } public Bitmap getBitmapFromDiskCache(String key) { synchronized (diskCacheLock) { // Wait while disk cache is started from background thread while (diskCacheStarting) { try { diskCacheLock.wait(); } catch (InterruptedException e) {} } if (diskLruCache != null) { return diskLruCache.get(key); } } return null; } // Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. public static File getDiskCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
Catatan: Bahkan tindakan seperti melakukan inisialisasi cache disk juga memerlukan operasi disk dan, oleh karena itu, tidak boleh dilakukan di thread utama. Akan tetapi, ini artinya ada kemungkinan cache diakses sebelum diinisialisasi. Untuk mengatasinya, pada implementasi di atas, objek kunci memastikan bahwa aplikasi tidak membaca dari cache disk hingga cache telah diinisialisasi.
Saat cache memori diperiksa di thread UI, cache disk diperiksa di thread latar belakang. Operasi disk tidak boleh dilakukan di UI thread. Saat pemrosesan gambar selesai, bitmap akhir akan ditambahkan ke cache memori dan cache disk untuk digunakan di masa mendatang.
Menangani Perubahan Konfigurasi
Perubahan konfigurasi runtime, seperti perubahan orientasi layar, menyebabkan Android menghancurkan dan memulai ulang aktivitas yang sedang berjalan dengan konfigurasi baru (Untuk informasi selengkapnya tentang perilaku ini, lihat Menangani Perubahan Runtime). Sebaiknya hindari memproses ulang semua gambar agar pengguna memiliki pengalaman yang lancar dan cepat saat terjadi perubahan konfigurasi.
Untungnya, Anda memiliki cache memori bitmap yang baik, yang telah Anda bangun di bagian Menggunakan Cache Memori. Cache ini dapat diteruskan ke
instance aktivitas baru menggunakan Fragment
yang dipertahankan dengan memanggil setRetainInstance(true)
. Setelah aktivitas
dibuat ulang, Fragment
yang dipertahankan ini dilampirkan ulang dan Anda dapat memperoleh akses ke
objek cache yang sudah ada sehingga gambar dapat diambil dengan cepat dan diisi ulang ke objek ImageView
.
Berikut contoh cara mempertahankan objek LruCache
saat terjadi perubahan
konfigurasi menggunakan Fragment
:
Kotlin
private const val TAG = "RetainFragment" ... private lateinit var mMemoryCache: LruCache<String, Bitmap> override fun onCreate(savedInstanceState: Bundle?) { ... val retainFragment = RetainFragment.findOrCreateRetainFragment(supportFragmentManager) mMemoryCache = retainFragment.retainedCache ?: run { LruCache<String, Bitmap>(cacheSize).also { memoryCache -> ... // Initialize cache here as usual retainFragment.retainedCache = memoryCache } } ... } class RetainFragment : Fragment() { var retainedCache: LruCache<String, Bitmap>? = null companion object { fun findOrCreateRetainFragment(fm: FragmentManager): RetainFragment { return (fm.findFragmentByTag(TAG) as? RetainFragment) ?: run { RetainFragment().also { fm.beginTransaction().add(it, TAG).commit() } } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } }
Java
private LruCache<String, Bitmap> memoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment retainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager()); memoryCache = retainFragment.retainedCache; if (memoryCache == null) { memoryCache = new LruCache<String, Bitmap>(cacheSize) { ... // Initialize cache here as usual } retainFragment.retainedCache = memoryCache; } ... } class RetainFragment extends Fragment { private static final String TAG = "RetainFragment"; public LruCache<String, Bitmap> retainedCache; public RetainFragment() {} public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG); if (fragment == null) { fragment = new RetainFragment(); fm.beginTransaction().add(fragment, TAG).commit(); } return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }
Untuk mengujinya, coba putar perangkat dengan dan tanpa mempertahankan Fragment
. Jika mempertahankan cache, Anda akan merasakan sedikit atau tidak ada jeda sama sekali
saat gambar mengisi aktivitas hampir seketika dari memori. Gambar yang tidak ditemukan di cache memori
mungkin ada di cache disk. Jika tidak, gambar tersebut akan diproses seperti biasa.