Development for Android
8 December 2011

Универсальный ImageLoader для Android

From Sandbox
В этой статье Я расскажу о разработанном мной (и не только) инструменте для асинхронной подгрузки изображений, их кэширования и отображения. На данном этапе развития его можно использовать повсеместно, где надо загрузить картинку в ImageView из интернета или из файловой системы. Все, что нужно, это URL картинки (для файловой системы он будет начинаться на «file://») и собственно ImageView, в который загруженную картинку надо будет положить. Более подробно о возможностях универсального разработанного ImageLoader'а читайте ниже.Началось все в одном проекте, в котором мне довелось учавствовать: необходимо было реализовать просмотр новостей списком.И тут конечно встал вопрос об отображении картинок в элементах списка. Картинки подгружались из интернета, поэтому надо было реализовать их асинхронную подгрузку, отображение и кэширование. Беглый поиск в сети навел меня на следующее почти готовое решение данной задачи. Реализованный LazyImageLoader асинхронно загружал картинки из интернета, кэшировал их в файловой системе, а также хранил их в памяти. Способ хранения в памяти представлял собой простой HashMap без каких-либо слабых ссылок, в результате чего на определенном этапе прокрутки списка (а списков к тому же было много) стал вылетать OutOfMemoryError. HashMap был заменен на WeakValueHashMap, а затем на собственную реализацию Map с ограничением на использование памяти.Постепенно, на базе этого LazyImageLoader'а стал вырастать собственный ImageLoader со своими фишечками и рюшечками. Его можно было использовать для отображения картинок не только в списках, но и в галерее, и для простого «одноразового» отображения. Этот ImageLoader был в дальнейшем переиспользован в двух других проектах, что подтвердило его состоятельность. Значительно отрефакторив существующий код и наведя приемлемую красоту, Я выложил исходники на GitHub, где сейчас постепенно ведется дальнейшая оптимизация интсрумента, повышение гибкости и настраиваемости.Итак, что все таки может этот ImageLoader?Отображать картинки — это ясно. Что насчет кэширования?Кэширование разделено на:
  • кэширование в памяти
  • кэширование на файловой системе (память телефона или SD-карта)
В роли кэша в памяти выступает HashMap<String, Bitmap> со «слабыми» ссылками в значениях. Насколько «слабыми» (Soft, Weak, Phantom) — решать вам:
public abstract class Cache<K, V> {

	protected final Map<K, Reference<V>> softMap = new HashMap<K, Reference<V>>();

	public V get(K key) {
		if (softMap.containsKey(key)) {
			Reference<V> reference = softMap.get(key);
			return reference.get();
		} else {
			return null;
		}
	}

	public void put(K key, V value) {
		softMap.put(key, createReference(value));
	}

	public void clear() {
		softMap.clear();
	}

	protected abstract Reference<V> createReference(V value);
}
В текущей версии используется кэш Bitmap'ов, контролирующий свой размер. Это было реализовано посредством введения дополнительного «жесткого» списка, где хранились «сильные» ссылки на Bitmap'ы из softMap'ы. Как только размер кэша превышает допустимый лимит, «самые старые» объекты удаляются из «жесткого списка», тем самым теряя сильную ссылку. Слабая ссылка все ещё сохраняется в softMap'e, но там Bitmap уже полностью во власти Garbage Collector'a.При кэшировании на файловой системе файлы именуются как imageUrl.hashCode() и в дальнейшем по такому же принципу проводится поиск в кэше.Рассмотрим самый полнофункциональный метод ImageLoader'а — это:
void displayImage(String imageUrl, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener)
Параметры imageUrl и imageView, Я думаю, вопросов не вызовут.Класс DisplayImageOptions предназначен для настройки процесса загрузки, кэширования и отображения картинки. С помощью него можно указать:
  • надо ли отображать картинку-заглушку в ImageView, пока загружается реальная картинка, и какую именно заглушку отображать;
  • надо ли кэшировать загруженную картинку в памяти;
  • надо ли кэшировать загруженную картинку на файловой системе.
Интерфейс ImageLoadingListener позволяет «слушать» процесс загрузки изображения:
public interface ImageLoadingListener {
    void onLoadingStarted();
    void onLoadingComplete();
}
Но если текущая картинка присутствует в кэше в памяти, то listener не будет бросать события. События бросаются на UI-потоке, так что можно со спокойной душой трогать UI в listener'e.Итак, пример использования ImageLoader'a:
ImageLoader imageLoader = ImageLoader.getInstance(context);
DisplayImageOptions options = new DisplayImageOptions.Builder()
                                       .showStubImage(R.drawable.stub_image)
                                       .cacheInMemory()
                                       .cacheOnDisc()
                                       .build();
imageLoader.displayImage(imageUrl, imageView, options, new ImageLoadingListener() {
    @Override
    public void onLoadingStarted() {
       spinner.show();
    }
    @Override
    public void onLoadingComplete() {
        spinner.hide();
    }
});
Сильно распространяться про сам механизм работы ImageLoader'а не буду. Скажу только пару вещей:
  • задания на отображение картинки кладутся в очередь: если картинка уже есть в кэше на файловой системе, задание попадает на начало очереди, если нет — в конец. Задания выполняются с начала очереди, тем самым отображая в первую очередь закэшированные картинки; (UPD: После введения многопоточного механизма отображения картинок данная логика была упразднена. Теперь загрузкой закэшированных и незакэшированный изображений занимаются два разных пула потоков: для закэшированных — однопоточный, для остальных — многопоточный)
  • в кэше в памяти хранятся не полноразмерные Bitmap'ы, а размера не менее того, который нужен для отображения в ImageView. Этот размер вычисляется, исходя из атрибутов maxWidth и maxHeight, layout_width и layout_height, размеров экрана аппарата (размер исходной картинки уменьшается на степень двойки, в соответствии с рекомендациями по декодированию изображений).
  • т.к. в первую очередь ImageLoader предназначается для отображения картинок в списке, а в списках, как правило, хорошим тоном являтся переиспользование View, то и ImageLoader отслеживает такие ситуации, сохраняя загружаемый URL картинки в Tag ImageView с собственным ключом.
Ещё раз дам ссылку на исходники на GitHub.Надеюсь данный ImageLoader пригодится и Вам.

UPD (19.12.2011): В инструмент были внесены некторые существенные изменения, подробно о них и о проекте в целом можно прочитать здесь.
UPD (23.02.2012): Была сделана куча изменений и улучшений (в т.ч. многопоточность, внешнее конфигурирование). Но основное API в принципе все то же. Теперь инструмент доступен в качетве jar-ки. Введена версионность.
UPD (11.03.2012): Написал подробное руководство по использованию библиотеки:

+29
29k 199
Comments 28
Top of the day