Not: Çoğu durumda, uygulamanızda bit eşlemleri getirmek, kodunu çözmek ve görüntülemek için Glide kitaplığını kullanmanızı öneririz. Kaydırarak, Android'de bit eşlemler ve diğer görsellerle çalışmayla ilgili bu görevleri ve diğer görevleri ele alma karmaşıklığını büyük ölçüde ortadan kaldırabilirsiniz. Glide'ı kullanma ve indirme hakkında bilgi edinmek için GitHub'daki Glide deposunu ziyaret edin.
Kullanıcı arayüzünüze (UI) tek bir bit eşlem yüklemek kolaydır, ancak tek seferde daha büyük bir görüntü grubu yüklemeniz gerektiğinde işler daha karmaşık hale gelir. Çoğu durumda (ListView
, GridView
veya ViewPager
gibi bileşenlerde olduğu gibi), yakında ekrana kaydırılabilecek resimlerle birlikte ekrandaki toplam resim sayısı aslında sınırsızdır.
Bunun gibi bileşenlerle, alt öğeler ekran dışına taşınırken geri dönüştürülerek bellek kullanımı düşük tutulur. Çöp toplayıcı, aynı zamanda uzun ömürlü referansları saklamadığınız varsayılarak, yüklenen bit eşlemlerinizi de serbest bırakır. Bunlar her şey yolundadır. Ancak akıcı ve hızlı yüklenen bir kullanıcı arayüzü tutmak için bu görüntüler ekrana her geri geldiğinde sürekli olarak işlemekten kaçınmanız gerekir. Bellek ve disk önbelleği, bileşenlerin işlenen resimleri hızlı bir şekilde yeniden yükleyebilmesini sağlayarak bu noktada genellikle yardımcı olabilir.
Bu derste, birden çok bit eşlem yüklerken kullanıcı arayüzünüzün duyarlılığını ve akıcılığını iyileştirmek için bellek ve disk bit eşlemi önbelleği kullanma hakkında bilgi verilmektedir.
Önbellek Kullanın
Bellek önbelleği, değerli uygulama belleğini kullanma pahasına bit eşlemlere hızlı erişim sağlar. LruCache
sınıfı (API Düzeyi 4'e geri dönmek için Destek Kitaplığı'nda da mevcuttur) özellikle bit eşlemleri önbelleğe alma, son başvurulan nesneleri güçlü bir referans verilen LinkedHashMap
içinde tutma ve önbellek belirtilen boyutu aşmadan en az kullanılan üyeyi çıkarma görevlerine çok uygundur.
Not: Geçmişte popüler bir önbellek uygulaması, SoftReference
veya WeakReference
bit eşlem önbelleğiydi, ancak bu önerilmez. Çöp toplayıcı, Android 2.3'ten (API Düzeyi 9) itibaren hafif/zayıf referanslar toplama konusunda daha agresif davranmaktadır ve bu nedenle bu referanslar oldukça etkisizdir. Ayrıca, Android 3.0'dan (API Düzeyi 11) önce, bit eşlemin yedek verileri yerel bellekte depolanıyordu. Bu veriler tahmin edilebilir bir şekilde serbest bırakılıyordu. Bu da uygulamaların bellek sınırlarını kısa süreliğine aşmasına ve kilitlenmesine neden oluyordu.
LruCache
için uygun bir boyut seçerken aşağıdaki faktörler dikkate alınmalıdır:
- Etkinliğinizin ve/veya uygulamanızın geri kalanında belleği ne kadar yoğun olarak kullanıyorsunuz?
- Ekranda aynı anda kaç resim gösterilir? Kaç kaç tanesinin ekrana gelmeye hazır olması gerekiyor?
- Cihazın ekran boyutu ve yoğunluğu ne? Galaxy Nexus gibi ekstra yüksek yoğunluklu ekran (xhdpi) cihazlar, Nexus S (hdpi) gibi cihazlarla aynı sayıda resmi bellekte tutmak için daha büyük bir önbelleğe gerek duyar.
- Bit eşlemler hangi boyutlar ve yapılandırmadır ve dolayısıyla her biri ne kadar bellek kaplar?
- Resimlere ne sıklıkta erişilecek? Bazılarına diğerlerine göre daha sık mı erişilecek?
Öyleyse belirli öğeleri her zaman bellekte tutmak, hatta farklı bit eşlem grupları için birden fazla
LruCache
nesnesi bulundurmak isteyebilirsiniz. - Nicelik ile nicelik arasında bir denge kurabilir misiniz? Bazen daha fazla sayıda düşük kaliteli bit eşlemi depolamak, başka bir arka plan görevinde daha yüksek kaliteli bir sürüm yüklemek daha yararlı olabilir.
Tüm uygulamalara uyan belirli bir boyut veya formül yoktur. Kullanımınızı analiz edip uygun bir çözüm bulmak size kalmıştır. Çok küçük bir önbellek, hiçbir faydası olmayan ek yüke neden olur. Çok büyük bir önbellek de yine java.lang.OutOfMemory
istisnalarına neden olabilir ve uygulamanızın geri kalanında üzerinde çok az bellek bırakılmasına neden olabilir.
Bit eşlemler için LruCache
ayarlama örneği aşağıda verilmiştir:
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); }
Not: Bu örnekte uygulama belleğinin sekizde biri önbelleğimize ayrılmıştır. Normal/hdpi cihazlarda bu boyut en az 4 MB'tır (32/8). 800x480 çözünürlüğe sahip bir cihazda resimlerle doldurulmuş tam ekran bir GridView
, yaklaşık 1,5 MB (800*480*4 bayt) kullanır. Böylece bellekte en az 2,5 sayfalık resmi önbelleğe alır.
ImageView
içine bit eşlem yüklenirken, önce LruCache
kontrol edilir. Bir giriş bulunursa ImageView
öğesini güncellemek için bu giriş hemen kullanılır. Aksi takdirde, resmin işlenmesi için bir arka plan iş parçacığı oluşturulur:
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); } }
Bellek önbelleğine giriş eklemek için BitmapWorkerTask
'in de güncellenmesi gerekir:
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; } ... }
Disk Önbelleği Kullanma
Önbellek önbelleği, son görüntülenen bit eşlemlerine erişimi hızlandırmaya yardımcı olur, ancak bu önbellekte bulunan resimlere güvenemezsiniz. Daha büyük veri kümelerine sahip GridView
gibi bileşenler, bir önbelleği kolayca doldurabilir. Uygulamanız, telefon araması gibi başka bir görev nedeniyle kesintiye uğratılabilir ve arka planda çalışırken sonlandırılabilir ve bellek önbelleği yok edilebilir. Kullanıcı devam ettirdikten sonra uygulamanızın her bir resmi yeniden işlemesi gerekir.
Bu gibi durumlarda, işlenen bit eşlemleri korumak ve resimlerin artık bellek önbelleğinde bulunmadığı yükleme sürelerini azaltmak için disk önbelleği kullanılabilir. Elbette diskten görüntü getirmek, bellekten yüklemekten daha yavaştır ve disk okuma süreleri öngörülemez olabileceği için bir arka plan iş parçacığında yapılmalıdır.
Not: ContentProvider
, resim galerisi uygulamasında olduğu gibi daha sık erişiliyorsa önbelleğe alınan resimleri depolamak için daha uygun bir yer olabilir.
Bu sınıfın örnek kodu, Android kaynağından çekilen bir DiskLruCache
uygulaması kullanır.
Mevcut önbellek önbelleğine ek olarak disk önbelleği ekleyen, güncellenmiş örnek kod aşağıda gösterilmektedir:
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); }
Not: Disk önbelleğinin başlatılması bile disk işlemleri gerektirir ve bu nedenle ana iş parçacığında gerçekleşmemelidir. Ancak bu durum, önbelleğe başlatma işleminden önce erişilebileceği anlamına gelir. Bu sorunu gidermek için yukarıdaki uygulamada kilit nesnesi, önbellek başlatılıncaya kadar uygulamanın disk önbelleğinden okumamasını sağlar.
Bellek önbelleği, kullanıcı arayüzü iş parçacığında kontrol edilirken, disk önbelleği arka plan iş parçacığında kontrol edilir. Disk işlemleri, hiçbir zaman kullanıcı arayüzü iş parçacığında gerçekleşmemelidir. Görüntü işleme tamamlandığında, nihai bit eşlem gelecekte kullanılmak üzere hem belleğe hem de disk önbelleğine eklenir.
Yapılandırma Değişikliklerini İşleme
Ekran yönü değişikliği gibi çalışma zamanı yapılandırma değişiklikleri, Android'in çalışan etkinliği yok edip yeni yapılandırmayla yeniden başlatmasına neden olur (Bu davranış hakkında daha fazla bilgi edinmek için Çalışma Zamanı Değişikliklerini İşleme bölümüne bakın). Yapılandırma değişikliği yapıldığında kullanıcının sorunsuz ve hızlı bir deneyim yaşaması için tüm resimlerinizi tekrar işlemek zorunda kalmamak istersiniz.
Neyse ki Bellek Önbelleği Kullanma bölümünde oluşturduğunuz bit eşlemlerden oluşan iyi bir bellek önbelleğine sahipsiniz. Bu önbellek, setRetainInstance(true)
çağrısıyla korunan bir Fragment
kullanılarak yeni etkinlik örneğine geçirilebilir. Etkinlik yeniden oluşturulduktan sonra, saklanan bu Fragment
tekrar eklenir ve mevcut önbellek nesnesine erişim elde edersiniz. Böylece, görüntülerin hızlı bir şekilde getirilip ImageView
nesnelerine yeniden doldurulmasını sağlarsınız.
Yapılandırma değişikliklerinde Fragment
kullanılarak bir LruCache
nesnesinin tutulmasına dair bir örneği burada bulabilirsiniz:
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); } }
Bunu test etmek için bir cihazı Fragment
ile birlikte ve tutmadan döndürmeyi deneyin. Önbelleği sakladığınızda resimler, etkinliği neredeyse anında bellekten doldurduğundan, gecikme çok az olur veya hiç gecikme olmaz. Önbellekte bulunmayan tüm resimler, disk önbelleğinde bulunur. Disk önbelleğinde yoksa her zamanki gibi işlenirler.