Bit eşlemleri Önbelleğe Alma

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.