Кэширование изображений

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

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

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

Использование кэш-памяти

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

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

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

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

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

Пример использования класса LruCache для изображений:

Примечание: в примере одна восьмая доступной памяти выделена под кэш. На устройствах с hdpi экранами это минимум 4Мб (32/4). Элемент GridView, растянутый на весь экран на устройстве с разрешением 800×480 будет использовать около 1.5Мб(800*480*4 байт), поэтому размера кэша должно хватать минимум на 2.5 страницы изображений.

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

Также необходимо обновить наш класс BitmapWorkerTask из предыдущего урока, чтобы добавлять записи в кэш-память:

Использование дискового кэша

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

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

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

Примеры кода в данном уроке используют класс DiskLruCache, который доступен здесь. Далее приведен обновленный пример кода, который добавляет дисковый кэш в дополнение к существующей кэш-памяти:

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

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

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

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

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

Пример применения объекта LruCache при изменении конфигурации, с использованием фрагментов:

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

Добавить комментарий