Bergabunglah bersama kami di ⁠#Android11: The Beta Launch Show pada tanggal 3 Juni!

Menyimpan Cache Bitmap

Catatan: Untuk sebagian besar kasus, kami merekomendasikan Anda menggunakan 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 tentang 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. Pengumpul sampah juga mengosongkan bitmap yang dimuat, dengan asumsi Anda tidak menyimpan referensi yang tahan 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 biasanya dapat bermanfaat di sini, sehingga komponen dapat dengan cepat memuat ulang gambar yang diproses.

Pelajaran ini akan memandu Anda menggunakan memori dan cache bitmap disk untuk menyempurnakan 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. 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 sehingga 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 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 ada sejumlah gambar yang akan lebih sering diakses daripada gambar 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? Kadang akan lebih bermanfaat untuk menyimpan bitmap berkualitas lebih rendah dalam jumlah lebih besar, 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 ada manfaatnya, dan cache yang terlalu besar dapat menyebabkan pengecualian java.lang.OutOfMemory, sehingga 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, memori minimum 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), jadi ini akan menyimpan cache minimum sekitar 2,5 halaman gambar dalam memori.

Saat memuat bitmap ke ImageView, LruCache akan diperiksa terlebih dahulu. Jika entri 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 tiap gambar lagi.

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, mengambil gambar dari disk prosesnya akan lebih lambat daripada memuat dari memori dan harus dilakukan di thread latar belakang, karena waktu baca disk tidak dapat diprediksi.

Catatan: ContentProvider mungkin jadi tempat yang lebih cocok untuk menyimpan gambar cache jika lebih sering diakses, misalnya dalam aplikasi galeri gambar.

Contoh kode class ini menggunakan implementasi DiskLruCache yang diambil dari sumber Android. Berikut ini contoh kode yang menambahkan cache disk selain 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 thread UI. Saat pemrosesan gambar selesai, bitmap akhir akan ditambahkan ke memori dan cache disk untuk digunakan di masa mendatang.

Menangani Perubahan Konfigurasi

Perubahan konfigurasi waktu proses, 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 Waktu Proses). 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 buat 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 Anda 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.