Bit eşlemleri Önbelleğe Alma

Not: Çoğu durumda aşağıdakileri yapmanızı öneririz: Glide aracını uygulamanızda bit eşlemler getirmek, kodunu çözmek ve görüntülemek için kitaplık. Özetlerin çoğunu kaydırarak ve bunları ele almanın karmaşıklığını başka görevler de vardır. Glide'ı kullanma ve indirme hakkında bilgi edinmek için şu sayfayı ziyaret edin: GitHub'da Glide deposu.

Kullanıcı arayüzünüze tek bir bit eşlem yüklemek oldukça basittir ancak işler giderek artmaktadır tek seferde daha büyük bir resim grubunu yüklemeniz gerekiyorsa karmaşık hale gelir. Birçok durumda (örneğin, ListView, GridView veya ViewPager gibi bileşenleri), toplam resim sayısı ile ekranı hızlıca kaydırabilme olanağı da aslında sınırsızdır.

Bunun gibi bileşenlerle, çocukların görünümleri hareket ederken geri dönüştürülerek bellek kullanımı azaltılır. ekranın dışına çıkar. Çöp toplayıcı, uzun süreli referanslar tutmadığınız varsayılarak yüklenen bitmap'lerinizi de boşaltır. Tüm bunlar gayet normal, ancak akıcı ve hızlı yüklenen bir kullanıcı arayüzünü korumak için bu resimlerin ekrana her geri geldiklerinde sürekli olarak işlenmesini önlemek istiyorsunuz. Anılar ve disk önbelleği genellikle bu konuda işe yarayabilir. Bu sayede, bileşenler, işlenen görüntüleri hızlıca yeniden yükleyebilir.

Bu derste, yanıt verme hızını artırmak için bellek ve disk bit eşlem önbelleği kullanma konusunda ve akıcılığınızı konuşturmak için kullanabileceğiniz birkaç teknik var.

Önbellek Kullan

Bellek önbelleği, değerli uygulama belleğini kullanma pahasına bitmap'lere hızlı erişim sağlar. LruCache sınıfı (geri kullanılmak üzere Destek Kitaplığı'nda da mevcuttur) özellikle de bit eşlemleri önbelleğe alma görevine uygundur. Güçlü referanslı LinkedHashMap içinde referans verilen nesnelerden en az son kullanılan üyeye ulaşılmasını sağlayın.

Not: Geçmişte popüler bir bellek önbelleği uygulaması Bununla birlikte, SoftReference veya WeakReference bit eşlem önbelleği bu önerilmez. Android 2.3 (API Düzeyi 9) sürümünden itibaren çöp toplayıcı yumuşak/zayıf referanslar toplama konusunda agresif ve bu durum onları oldukça etkisiz hale getirir. Ayrıca, Önceden bir bit eşlemin yedekleme verileri yerel bellekte depolanıyordu. tahmin edilebilir bir şekilde yayınlanamaması, bu durumun uygulama ve çökmeye neden olabilir.

LruCache için uygun bir beden seçerken çeşitli faktörleri dikkate alın. dikkate alınmalıdır. Örneğin:

  • Etkinliğinizin ve/veya uygulamanızın geri kalanı bellek ne kadar yoğun?
  • Ekranda aynı anda kaç resim olacak? Kaç kişinin hizmet vermeye hazır olması gerekir? ekranda mı?
  • Cihazın ekran boyutu ve yoğunluğu nedir? Ekstra yüksek yoğunluklu ekran (xhdpi) cihaz Galaxy Nexus için bir ekran Nexus S (hdpi) gibi cihazlarla karşılaştırıldığında aynı sayıda resmi tutmak için daha büyük bir önbellek sunar.
  • Hangi boyutlar ve yapılandırmalar bit eşlemlerdir ve dolayısıyla her biri için ne kadar bellek yükseldi?
  • Resimlere ne sıklıkta erişilecek? Bazılarına diğerlerine göre daha sık erişilecek mi? Öyleyse bazı öğeleri her zaman bellekte tutmak, hatta farklı bit eşlem grupları için birden fazla LruCache nesnesi kullanmak isteyebilirsiniz.
  • Nitelik ile niceliği dengeleyebilir misiniz? Bazen, daha büyük bir dosyayı saklamak daha düşük kaliteli bit eşlem sayısı ve potansiyel olarak başka bir arka plan görevi var.

Tüm uygulamalara uygun belirli bir boyut veya formül yoktur. Kullanımınızı analiz edip uygun bir çözüm bulmak size bağlıdır. Çok küçük bir önbellek, işe yaramaz, çok büyük bir önbellek yine java.lang.OutOfMemory istisnalarına neden olabilir ve uygulamanızın geri kalanında çalışmak için çok az bellek olur.

Aşağıda, bit eşlemler için bir LruCache ayarlama örneği 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 ayırdığımızı göreceksiniz. Normal/hdpi cihazlarda bu boyut yaklaşık 4 MB'tır (32/8). 800x480 çözünürlüğe sahip bir cihazda resimlerle dolu tam ekran GridView yaklaşık 1,5 MB (800*480*4 bayt) kullanır. Bu nedenle, en az 2,5 sayfalık resim bellekte önbelleğe alınır.

ImageView öğesine bit eşlem yüklerken LruCache olup olmadığına bakın. Bir giriş bulunursa ImageView öğesini güncellemek için hemen kullanılır. Aksi takdirde, resmin işlenmesi için bir arka plan ileti dizisi 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);
    }
}

BitmapWorkerTask ayrıca bellek önbelleğine giriş eklemek üzere güncellendi:

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

Bellek önbelleği, en son görüntülenen bitmap'lere erişimi hızlandırmak için kullanışlıdır ancak resimlerin bu önbellekte bulunacağına güvenemezsiniz. Şunları içeren GridView gibi bileşenler: büyük veri kümeleri, önbelleği kolayca doldurabilir. Başvurunuz başka bir nedeniyle kesintiye uğrayabilir yapmalarına yardımcı olabilir. Arka planda çalışırken kapanabilir ve önbelleğin yok. Kullanıcı devam ettiğinde uygulamanızın her resmi tekrar işlemesi gerekir.

Bu durumlarda, işlenen bit eşlemleri korumak ve yüklemeyi azaltmaya yardımcı olmak için disk önbelleği kullanılabilir. görüntülerin artık bellek önbelleğinde kullanılamadığı zamanlar. Elbette, diskten resim getirme işlemi bellekten yüklemekten daha yavaştır ve disk okuma süreleri tahmin edilemez olabileceğinden arka plan iş parçacığında yapılmalıdır.

Not: ContentProvider, sizin için daha uygun olabilir daha sık erişilirse önbelleğe alınmış resimlerin depolanması için uygun bir yere resim galerisi uygulamasıdır.

Bu sınıfın örnek kodunda, Android kaynağından alınan bir DiskLruCache uygulaması kullanılmaktadır. Mevcut bellek önbelleğine ek olarak bir disk önbelleği ekleyen güncellenmiş örnek kodu burada bulabilirsiniz:

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ğini başlatmak için bile disk işlemleri gerekir. Bu nedenle, ana ileti dizisinde yer almamalıdır. Ancak bu, bazı durumlarda önbelleğe alma işlemi başlatmadan önce yapılır. Bu sorunu gidermek için, yukarıdaki uygulamada nesne, önbellek tamamlanana kadar uygulamanın disk önbelleğinden okuma yapmamasını sağlar. başlatıldı.

Bellek önbelleği, kullanıcı arayüzü iş parçacığında kontrol edilirken disk önbelleği arka planda kontrol edilir ileti dizisi. Disk işlemleri, kullanıcı arayüzü iş parçacığı üzerinde hiçbir zaman gerçekleşmemelidir. Resim işleme, tamamlandığında son bit eşlem, ileride 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 yeni yapılandırmayla yeniden başlatın (Bu davranış hakkında daha fazla bilgi için Çalışma Zamanı Değişikliklerini İşleme bölümüne bakın). Kullanıcıların sorunsuz ve hızlı bir deneyim yaşaması için tüm resimlerinizi tekrar işlemek zorunda kalmamak istiyorsunuz. bir deneyim anlamına gelir.

Neyse ki Bellek Önbelleği Kullanma bölümünde oluşturduğunuz güzel bir bitmap bellek önbelleğiniz var. Bu önbellek, setRetainInstance(true) çağrılarak korunan bir Fragment kullanılarak yeni etkinlik örneğine iletilebilir. Etkinlik oluşturulduktan sonra yeniden oluşturulursa, saklanan bu Fragment tekrar eklenir ve siz de mevcut önbellek nesnesi bulunur. Bu nesne, resimlerin hızlı bir şekilde getirilmesini ve ImageView nesnelerine yeniden doldurulmasını sağlar.

Yapılandırma genelinde LruCache nesnesinin tutulmasıyla ilgili bir örneği burada bulabilirsiniz. Fragment kullanılarak yapılan değişiklikler:

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 Fragment cihazını tutmadan veya tutmadan cihazı döndürmeyi deneyin. Resimler neredeyse etkinliği doldurduğundan gecikmenin çok düşük olduğunu veya hiç olmadığını fark edeceksiniz. anında bellekten otomatik olarak alırsınız. Bellek önbelleğinde bulunmayan resimler, yoksa disk önbelleğinde bulunmayıp her zamanki gibi işlenir.