Кеширование изображений на SD карте

Совсем недавно пользователь sly2m описал свой метод сохранения изображений из ImageView на SD карту телефона. Кто-то (лично я например) ожидал от этого поста нечто иное, а именно:

1. Работа с изображениями из Интернета
2. Автоматическая загрузка и сохранение таких изображений
3. Продвинутое кеширование изображений

Если заинтересовало — прошу взглянуть.

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

Для начала создадим каркас класса и парочку дополнительных методов, которые пригодятся нам далее:
package com.habra.imagemanager;

public class ImageManager {
	public String md5(String s) {
		try {
			MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
			digest.update(s.getBytes());
			byte messageDigest[] = digest.digest();
			StringBuffer hexString = new StringBuffer();
			for (int i = 0; i < messageDigest.length; i++) {
				hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
			}
			return hexString.toString();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return "";
	}
	public static void fileSave(InputStream is, FileOutputStream outputStream) {
		int i;
		try {
			while ((i = is.read()) != -1) {
				outputStream.write(i);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}


Назначение функции md5 думаю понятно, а вот функция fileSave будет сохранять любой InputStream в FileOutputStream, то-есть загружать наши картинки из сети и сохранять их на SD карту.

Далее, создаем вектор downloaded для хранения уже загруженных картинок, и метод для работы с таким вектором.
Это необходимо для устранения такого неприятного эффекта, как выполнение сразу нескольких потоков загрузки например для элементов ImageView внутри ListView.

private Vector<ImageView> downloaded = new Vector<ImageView>();
public boolean findObject(ImageView object) {
	for (int i = 0; i < downloaded.size(); i++) {
		if (downloaded.elementAt(i).equals(object)) {
			return true;
		}
	}
	return false;
}


Метод findObject будет искать поставленные в очередь картинки и возвращать true, если оная найдена.

А теперь два главных метода в нашем классе:

private Bitmap downloadImage(Context context, int cacheTime, String iUrl, ImageView iView) {
	Bitmap bitmap = null;
	if (cacheTime != 0) {
		File file = new File(context.getExternalCacheDir(), md5(iUrl)
				+ ".cache");
		long time = new Date().getTime() / 1000;
		long timeLastModifed = file.lastModified() / 1000;
		try {
			if (file.exists()) {
				if (timeLastModifed + cacheTime < time) {
					file.delete();
					file.createNewFile();
					fileSave(new URL(iUrl).openStream(),
							new FileOutputStream(file));
				}
			} else {
				file.createNewFile();
				fileSave(new URL(iUrl).openStream(), new FileOutputStream(
						file));
			}
			bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (bitmap == null) {
			file.delete();
		}
	} else {
		try {
			bitmap = BitmapFactory.decodeStream(new URL(iUrl).openStream());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	if (iView != null) {
		downloaded.remove(iView);
	}
	return bitmap;
}
public void fetchImage(final Context context, final int cacheTime, final String url, final ImageView iView) {
	if (iView != null) {
		if (findObject(iView)) {
			return;
		}
		downloaded.add(iView);
	}
	new AsyncTask<String, Void, Bitmap>() {
		protected Bitmap doInBackground(String... iUrl) {
			return downloadImage(context, cacheTime, iUrl[0], iView);
		}
		protected void onPostExecute(Bitmap result) {
			super.onPostExecute(result);
			if (iView != null) {
				iView.setImageBitmap(result);
			}
		}
	}.execute(new String[] { url });
}


Итак, метод downloadImage(Activity activity, int cacheTime, String iUrl, ImageView iView) выполняет саму загрузку изображений из Сети и ее кеширование.
Ее параметры:
  • Activity activity — активити приложения, нужно для определения пути для хранения кешированных файлов
  • int cacheTime — время релевантности кеша, в секундах
  • String iUrl — URL изображения
  • ImageView iView — ImageView, которому необходимо установить изображение


Но работать мы будем не с ним (не заметили модификатор private?), а с функцией fetchImage, которая принимает те-же самые параметры, что и у downloadImage. Сам метод fetchImage следит за списком загрузки и установку изображений в ImageView.
По моему скромному мнению, код интуитивно понятен и в комментариях не нуждается. В конце своего поста приведу пример использования класса:

ImageManager man = new ImageManager();

ImageView i1 = (ImageView) findViewById(R.id.i1);
ImageView i2 = (ImageView) findViewById(R.id.i2);
ImageView i3 = (ImageView) findViewById(R.id.i3);

man.fetchImage(this, 3600, "http://habrastorage.org/storage1/51624865/5d7f2b56/333c3c3f/fa5cdc9b.png", i1);
man.fetchImage(this, 3600, "http://habrastorage.org/storage1/9042dd3c/acc1f8b3/782ca380/c05ecaf3.png", i2);
man.fetchImage(this, 3600, "http://habrastorage.org/storage1/39a0bbce/4f56b8c7/ca84d78f/8b7bf972.png", i3);


UPDATE: Исходный код класса
UPDATE 2: Переписан метод fetchImage с использованием AsyncTask
UPDATE 3: Activity изменен на Context
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 26

    +1
    В методе fetchImage Вы только что изобрели AsyncTask.
      0
      Изобрел его явно не я, да и знаю я о нем. Просто привычка работать нативно с Thread, с j2me привычка то:)
        0
        если настаиваете — могу и переписать под AsyncTask, таки он удобнее, соглашусь
          0
          Переписывать не нужно. Просто хочется взглянуть еще и на вариант с AsyncTask.
            0
            public void fetchImage(final Activity activity, final int cacheTime, final String url, final ImageView iView) {
            if (iView != null) {
            if (findObject(iView)) {
            return;
            }
            downloaded.add(iView);
            }
            new AsyncTask<String, Void, Bitmap>() {
            protected Bitmap doInBackground(String... iUrl) {
            return downloadImage(activity, cacheTime, iUrl[0], iView);
            }
            protected void onPostExecute(Bitmap result) {
            super.onPostExecute(result);
            if (iView != null) {
            iView.setImageBitmap(result);
            }
            }
            }.execute(new String[] { url });
            }
              +1
              извиняюсь, в комменте вышло некрасиво, исправил в статье
          0
          > Activity activity — активити приложения, нужно для определения пути для хранения кешированных файлов

          Так все таки нужно Activity или достаточно Context?
          http://developer.android.com/reference/android/content/Context.html#getExternalCacheDir()
            0
            Достаточно Context
            0
            Еще можно сделать второй уровень: HashMap
            В качестве ключей — URL картинок.
              0
              Парсер куда-то потерял generic, там было HashMap<String, SoftReference<Bitmap>>
                0
                Тут дело не в картинках, а как я привел пример в статье: для ListView метод будет вызываться несколько раз для одного и того-же ImageView, соответственно будет плодится куча потоков с загрузкой одной и той же картинки
                +1
                Мне кажется есть некоторые нестыковки.
                1. getExternalCacheDir — вернет null если, например, телефон подключен через usb как девайс или если места недостаточно — думаю приложение упадет в данном случае
                2. Нет проверки размера файла и ресайза — думаю приложение упадет по переполнению буфера если попытается отобразить картинку 2048*1024
                3. Как то странно (если не сказать тупо) организовано время хранения файла. Ожидал чего то вроде чтения хидеров из http заголовков и выставления времени на основании expired cashe time или как там.
                4. Нет таймаута на запрос — зависнет?
                5. Как справедливо замечено выше — нет HashMap c ссылками для отображения изображений, например в listview, а ведь именно там и начинаются все пляски с onLowMemoryReceived, очистки кеша, recycle и т.д.

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

                На stackoverflow можно найти более юзабельные классы для загрузки изображений и доработать напильником. А ещё лучше посмотреть как реализована работа с изображениями в GreenDroid и сразу делать хорошо
                  0
                  1. Просто класс используется в приложении, которое отказывается запускаться без карты памяти, поэтому я не вводил проверку на существование карты памяти, а делается это двумя строчками ровно.
                  2. В основном используюю класс для иконок в ListView, и там таки размеры уж точно не нужны.
                  3. При таймауте должно выбить Exception и класс удалит файл кеша, если оный имеется.
                  4. Ну тут согласен
                    0
                    Ой, третий пункт это четвертый, а четвертый это пятый.
                    3. Иконки в проекте разделены на несколько групп, одни не меняются годами, другие раз в месяц, остальные вообще динамическиее. Поэтому все указывается в секундах
                  0
                  } catch (Exception e) {
                  e.printStackTrace();
                  }


                  Не айс…
                    0
                    Дальше код посмотрите:

                    if (bitmap == null) {
                    file.delete();
                    }

                    Тоесть если нет никакой картинки — удаляем
                      0
                      Я про перехват Exception вместо IOException и иных проверяемых исключений которые может выбрасывать ваш код. Меня, пришедшего с .NET, очень печалит когда на яве, имеющей такую прекрасную вещь как проверяемые исключения, люди плюют на человеческую обработку.
                        0
                        Тут при любом исключении требуется только одно — ничего не делать
                          0
                          К сожалению это не так. Например проглотить OutOfMemoryError — не лучшая идея. Другие варианты — возможные ошибки вашего кода (в общем случае), которые могут остаться незамеченными, вроде NullRef и т.д

                          Советую прочитать как можно больше про обработку исключений. Их необдуманное использование может создать вам массу проблем.
                    +1
                    Дело в том, что я писал статью "Пишем функцию сохранения картинок на SD-карту", а не «Качаем картинки из Инета и потом куда-нибудь их сохраняем при этом кэшируя». Вероятно по этому ваши ожидания не оправдались.
                    С другой стороны — это же хорошо. Неоправдавшиеся ожидания привели к новой статье с дополнительными деталями и объяснениями некоторых неохваченных доселе вопросов.

                    Что же до моего проекта, я в нем не мудрствую лукаво, и для скачивания картинок из интернета пользуюсь специальным довольно стандартным классом (AsyncTask, да) такого вот содержания:

                    public class ImageDownloader extends AsyncTask<String, Void, Bitmap> {
                    
                        private String url;
                        private final WeakReference<ImageView> imageViewReference;
                       
                        public ImageDownloader(ImageView imageView) {
                            imageViewReference = new WeakReference<ImageView>(imageView);
                        }
                    
                    // эта функция качает файло
                        @Override
                        protected Bitmap doInBackground(String... params) {
                        	url = params[0];
                        	URL myFileUrl = null;
                        	HttpURLConnection conn;
                        	InputStream is;
                        	
                     		try {
                     			myFileUrl= new URL(url);
                     		} catch (MalformedURLException e) { 			
                     			e.printStackTrace();
                     			return null;
                     		}
                     		try {
                     			conn = (HttpURLConnection)myFileUrl.openConnection();
                     			conn.setDoInput(true);
                     			conn.connect();
                     			is = conn.getInputStream();
                     			return BitmapFactory.decodeStream(is); 							 		
                     		catch (Exception e)
                     		{
                     			e.printStackTrace(); 			
                     			is = null;
                    			conn = null; 		
                     			return null;
                     		}
                        }
                    
                    // эта функция показывает скачанный файл в ImageView
                        @Override
                        protected void onPostExecute(Bitmap result) {
                            if (isCancelled()) {
                                result = null;
                            }
                            if (imageViewReference != null) {
                                ImageView imageView = imageViewReference.get();
                                if (imageView != null) {
                                    imageView.setImageBitmap(result);
                                }
                            }
                        }
                    
                    // Эта функция вызывается для того, чтобы показать пользователю что-то, пока грузится картинка.
                    // Например песочные часы или лучше прогрессбар
                        @Override
                        protected void onPreExecute() {
                            if (imageViewReference != null) {
                                ImageView imageView = imageViewReference.get();
                                if (imageView != null) {
                                    imageView.setImageResource(R.drawable.icon);
                                }
                            }
                        }
                    }
                    


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

                    ImageView iv;
                    ImageDownloader downloader = new ImageDownloader(iv);
                    downloader.execute(pictureURL);
                    

                      +2
                      Лучше вместо:

                      conn = (HttpURLConnection)myFileUrl.openConnection();
                      conn.setDoInput(true);
                      conn.connect();
                      is = conn.getInputStream();
                      return BitmapFactory.decodeStream(is); 	
                      


                      использовать такую конструкцию:

                      HttpGet req = null;
                      req = new HttpGet(url);
                      HttpClient _client = new DefaultHttpClient();
                      HttpResponse resp = (HttpResponse) _client.execute(req);
                      HttpEntity entity = resp.getEntity();
                      BufferedHttpEntity buffered_entity = new BufferedHttpEntity(entity);
                      InputStream is = buffered_entity.getContent();
                      BitmapFactory.decodeStream(is);
                      


                      Тогда и кэширование будет и периодически ошибка «decode returned false» не будет возникать.
                        0
                        То-есть для каждого ImageView класс ImageDownloader создается новый? Нехорошо конечно…
                          0
                          У меня один ImageView :)

                          А если их много — ну хочешь, можешь им pool сделать, это же AsyncTask, т.е. свой поток, хоть и специально сильно облегченный. Хочешь сразу во много потоков картинки грузить — создавай каждый раз новый, хочешь грузить только то, что видно юзеру — делай пул.
                        0
                        У вас проблемы с многопоточностью, и то что Vector засинхронизирован вас не спасёт. findObject никак не защищён от того, что во время цикла элемент может быть удылён другим потоком. И почему список downloaded это список ImageView (т.е. куда качаем), а не собственно того что мы качаем? Одна и та же картинка для разных ImageView будет качаться отдельно?
                          0
                          Опять же, этот класс я использую для элементов списка, метод fetchImage может вызываться несколько раз для одной и того-же ImageView, Vector создан исключительно для этого
                          0
                          Использовал ваш код у себя. У меня >300 изображений грузится во время обновления. В принципе они не большие, не более 500кб. Но весь процесс происходит до ужаса долго. Каждая картинка льется по 2-3 минуты. Не в курсе что могло послужить этому причиной?

                          Only users with full accounts can post comments. Log in, please.