Кэширование растровых изображений

Примечание. В большинстве случаев мы рекомендуем использовать библиотеку Glide для извлечения, декодирования и отображения растровых изображений в вашем приложении. Glide абстрагирует большую часть сложностей при выполнении этих и других задач, связанных с работой с растровыми изображениями и другими изображениями на Android. Для получения информации об использовании и загрузке Glide посетите репозиторий Glide на GitHub.

Загрузка одного растрового изображения в пользовательский интерфейс (UI) проста, однако все усложняется, если вам нужно одновременно загрузить больший набор изображений. Во многих случаях (например, с такими компонентами, как ListView , GridView или ViewPager ) общее количество изображений на экране в сочетании с изображениями, которые вскоре могут прокручиваться на экране, практически не ограничено.

Использование памяти с помощью таких компонентов снижается за счет повторного использования дочерних представлений по мере их перемещения за пределы экрана. Сборщик мусора также освобождает загруженные растровые изображения, при условии, что вы не храните долгоживущие ссылки. Это все хорошо, но для того, чтобы пользовательский интерфейс оставался гибким и быстро загружающимся, вам следует избегать постоянной обработки этих изображений каждый раз, когда они возвращаются на экран. Здесь часто могут помочь память и дисковый кеш, позволяющие компонентам быстро перезагрузить обработанные изображения.

В этом уроке вы узнаете, как использовать кэш растровых изображений в памяти и на диске, чтобы улучшить скорость реагирования и плавность пользовательского интерфейса при загрузке нескольких растровых изображений.

Используйте кэш памяти

Кэш-память обеспечивает быстрый доступ к растровым изображениям за счет использования ценной памяти приложения. Класс LruCache (также доступен в библиотеке поддержки для использования на уровне API 4) особенно хорошо подходит для задачи кэширования растровых изображений, сохранения объектов, на которые недавно ссылались, в LinkedHashMap с сильной ссылкой и удаления наименее недавно использованного члена до того, как кеш превысит его размер. обозначенный размер.

Примечание. Раньше популярной реализацией кэша памяти был кэш растровых изображений SoftReference или WeakReference , однако это не рекомендуется. Начиная с Android 2.3 (уровень API 9) сборщик мусора более агрессивно собирает мягкие/слабые ссылки, что делает их довольно неэффективными. Кроме того, до Android 3.0 (уровень API 11) резервные данные растрового изображения хранились в собственной памяти, которая не освобождалась предсказуемым образом, что потенциально могло привести к кратковременному превышению ограничений памяти и сбою приложения.

Чтобы выбрать подходящий размер для LruCache , следует принять во внимание ряд факторов, например:

  • Насколько требовательна к памяти остальная часть вашей деятельности и/или приложения?
  • Сколько изображений будет отображаться на экране одновременно? Сколько из них должно быть доступно для выхода на экран?
  • Какой размер экрана и плотность устройства? Устройству с экраном сверхвысокой плотности (xhdpi), такому как Galaxy Nexus, потребуется больший кэш для хранения такого же количества изображений в памяти, по сравнению с таким устройством, как Nexus S (hdpi).
  • Каковы размеры и конфигурация растровых изображений и, следовательно, сколько памяти они будут занимать?
  • Как часто будут доступны изображения? Будут ли некоторые из них доступны чаще, чем другие? Если да, возможно, вам захочется всегда хранить определенные элементы в памяти или даже иметь несколько объектов LruCache для разных групп растровых изображений.
  • Можете ли вы сбалансировать качество и количество? Иногда может быть более полезно хранить большее количество растровых изображений более низкого качества, потенциально загружая версию более высокого качества в другую фоновую задачу.

Не существует определенного размера или формулы, подходящей для всех применений, вам решать, проанализировать свое использование и найти подходящее решение. Слишком маленький кеш приводит к дополнительным накладным расходам без какой-либо выгоды, слишком большой кеш может снова вызвать исключения java.lang.OutOfMemory и оставить мало памяти для работы остальной части вашего приложения.

Вот пример настройки LruCache для растровых изображений:

Котлин

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
        }
    }
    ...
}

Ява

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);
}

Примечание. В этом примере под наш кэш выделена восьмая часть памяти приложения. На обычном устройстве/HDPI это минимум около 4 МБ (32/8). Полноэкранный GridView , заполненный изображениями, на устройстве с разрешением 800x480 будет использовать около 1,5 МБ (800*480*4 байта), поэтому в памяти будет кэшироваться минимум около 2,5 страниц изображений.

При загрузке растрового изображения в ImageView сначала проверяется LruCache . Если запись найдена, она немедленно используется для обновления ImageView , в противном случае создается фоновый поток для обработки изображения:

Котлин

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
    }
}

Ява

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 также необходимо обновить, чтобы добавить записи в кеш памяти:

Котлин

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)
            }
        }
    }
    ...
}

Ява

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;
    }
    ...
}

Используйте дисковый кэш

Кэш памяти полезен для ускорения доступа к недавно просмотренным растровым изображениям, однако вы не можете полагаться на наличие изображений в этом кэше. Такие компоненты, как GridView с большими наборами данных могут легко заполнить кеш-память. Ваше приложение может быть прервано другой задачей, например телефонным звонком, а в фоновом режиме оно может быть остановлено, а кеш памяти уничтожен. Как только пользователь возобновит работу, вашему приложению придется снова обработать каждое изображение.

В этих случаях можно использовать дисковый кэш для сохранения обработанных растровых изображений и сокращения времени загрузки, когда изображения больше не доступны в кэше памяти. Конечно, извлечение изображений с диска происходит медленнее, чем загрузка из памяти, и его следует выполнять в фоновом потоке, поскольку время чтения с диска может быть непредсказуемым.

Примечание. ContentProvider может быть более подходящим местом для хранения кэшированных изображений, если к ним обращаются чаще, например, в приложении галереи изображений.

Пример кода этого класса использует реализацию DiskLruCache , полученную из исходного кода Android . Вот обновленный пример кода, который добавляет дисковый кэш в дополнение к существующему кэшу памяти:

Котлин

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)
}

Ява

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);
}

Примечание. Даже инициализация дискового кэша требует дисковых операций и поэтому не должна выполняться в основном потоке. Однако это означает, что существует вероятность обращения к кешу до инициализации. Чтобы решить эту проблему, в приведенной выше реализации объект блокировки гарантирует, что приложение не будет читать данные из дискового кеша до тех пор, пока кеш не будет инициализирован.

В то время как кеш памяти проверяется в потоке пользовательского интерфейса, кэш диска проверяется в фоновом потоке. Дисковые операции никогда не должны выполняться в потоке пользовательского интерфейса. Когда обработка изображения завершена, окончательное растровое изображение добавляется как в память, так и в дисковый кэш для использования в будущем.

Обработка изменений конфигурации

Изменения конфигурации среды выполнения, такие как изменение ориентации экрана, приводят к тому, что Android уничтожает и перезапускает выполняемое действие с новой конфигурацией (дополнительную информацию об этом поведении см. в разделе Обработка изменений времени выполнения ). Вы хотите избежать повторной обработки всех изображений, чтобы пользователь мог беспрепятственно и быстро работать при изменении конфигурации.

К счастью, у вас есть хороший кэш растровых изображений, который вы создали в разделе «Использование кэша памяти» . Этот кеш можно передать новому экземпляру активности с помощью Fragment , который сохраняется путем вызова setRetainInstance(true) . После воссоздания активности этот сохраненный Fragment повторно присоединяется, и вы получаете доступ к существующему объекту кэша, что позволяет быстро получать изображения и повторно заполнять их объектами ImageView .

Вот пример сохранения объекта LruCache при изменении конфигурации с помощью Fragment :

Котлин

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
    }
}

Ява

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);
    }
}

Чтобы проверить это, попробуйте повернуть устройство как с сохранением Fragment , так и без него. Вы должны заметить небольшую задержку или вообще ее отсутствие, поскольку изображения почти мгновенно заполняют действие из памяти, когда вы сохраняете кеш. Любые изображения, не найденные в кеше памяти, мы надеемся, доступны в кеше диска, если нет, они обрабатываются как обычно.