InnovationM Lazy Loading Memory Management UITableView Android

Lazy Loading and Memory Management of Images in ListView in Android

Introduction to Lazy Loading

What is lazy loading?

A little introduction of lazy loading is  it is a design pattern to defer the initialization of an object until the point at which it is needed. In simple words create objects when it is needed.

Problems to Tackle

Before talking about lazy loading of images, I want to mention about problems / issues associated in handling images in ListView  / GridView in Android. These are:

  1. Scrolling is blocked / interrupted – Downloading images from server OR loading from device local storage is heavy task and it may take some time. If you load  images directly in getView() method of Adapter then it blocks the UI thread and your ListView scroll will be interrupted and not smooth. You  need a mechanism to load images in a worker (separate) thread and show a place holder image until the image in not downloaded and placed in memory for fast access. Remember accessing images from Hard disk may also takes some time.
  2. App Heap Memory can overshoot – If we load many images in memory for faster access by ListView / Adapter, then memory (heap memory) allocated to the application might overshoot and app will crash with Out of Memory (OOM) error.
  3. Work with Recycling of Views can be tricky – When image is  downloaded  we need to  decide when to set a particular image in its ImageView of that row it is meant for.  It may be possible that the ImageView object will recycle and it  is given to another image and we end up showing wrong image.

Solution

Memory Locations – Space Vs Accessibility:

There are 3 locations where images are stored. Their comparison on Memory space availability and Accessibility speed is given below:

  1. Server (Memory Space is HIGH but Accessibility is SLOW)
  2. Hard Disk on Mobile (Memory Space is MEDIUM and Accessibility is MEDIUM)
  3. Heap Memory (Memory Space is LOW and Accessibility is FAST)

We need to create a balance among above 3 locations for optimum utilization of accessibility and memory space.

Mappings:

4 Mappings are required to manage the show. I have given them names. These are

  1. Bitmap Cache Map  (Unique Id of Row TO Bitmap of Image)  – LruCache class provided in Android SDK to store images in memory (Heap) will be used here. LruCache will act as cache and will also recycle the images after a certain limit is reached.
  2. ImageView Recycler Map (Row ImageView Object TO Unique Id of Row) – We will maintain a mapping of ImageView and Id of Row to identify visible views and accordingly set images on ImageView.
  3. Image Loader / Downloader Queue (Unique Id of Row TO Row ImageView Object TO URL of Image)  –  Queue of requests to load images (if available) from device local storage to Bitmap Cache OR download images from server and store them in device local storage and then place in Bitmap Cache Map.
  4. Image Download Tracker (Image URL TO Status of Download) – This will map the URL and the status of image that is being downloaded from the URL. It is possible that request to download the same image comes and that image is already in process of downloading. This tracker will help to ignore such requests.

Sequence of Steps:

1. User comes to ListView. Adapter.getView() is fired for a single row. getView() will make an entry in ImageView Recycler Map with ImageView.
ImageView Recycler Map Entry:  – Key – IV1 (Object of ImageView), Value – ID1 ( Unique Id of row – Position of Row 

2. The image (ID1) is checked in Bitmap Cache Map. If the image is found in Bitmap Cache Map, it is set in ImageView and displayed. If the image is NOT found in Bitmap Cache Map, a place holder image (Default Image) is set in ImageView and a new request is queued to load it from device local storage OR download the image.
Queue Entry: Unique Id (ID1), ImageView (IV1) and URL of Image

3. Process the request from the Queue.

CHECKPOINT 1 of 3 – It is possible that by the time request from the Queue is picked up for loading the image from local storage or downloading image, ListView has been scrolled by the user and ImageView object is recycled and allocated to other row with a different ID. So, we don’t need to download this image as it is not currently shown. Mapping of ImageView (IV1) and Unique ID (ID1) is checked in ImageView Recycler Map for that.

4. Load the image into Image Bitmap Cache if image is available on device local storage else downloading image. This is to be done in new thread. If mapping (See CHECKPOINT above) exists, image loading / downloading from server starts else we simply return from thread and that request to load / download is dropped. 

5. If the image is available on device local storage Or it is available after downloading from server, it is put in Bitmap Cache  Map and send for display.

CHECKPOINT 2 of 3 – It is possible that by the time image is to be loaded from device local storage (Either already available OR after download), ListView has been scrolled by the user and ImageView object is recycled and allocated to other row with a different ID. Mapping of ImageView and ID is checked in ImageView Recycler Map for that. If ImageView has been recycled, we don’t need to place this image in Bitmap Cache Map. But keep it on hard disk.

6. To display the image on ImageView we post a Runnable on the UI Thread using the Handler.

CHECKPOINT 3 of 3 – Just before image is set on ImageView we again check in ImageView Recycler Map if the Mapping of ImageView (IV1) and Id (ID1) exists. If the mapping exists we set the image on ImageView else the place  holder (default) image is set.

I created Android library project for this and can import in Android application project. To use it you just have to instantiate an instance of ImageLoader class and call its method displayImage() from getView() method of Adapter.

Main Components:

  1. ImageLoader – This class provides Lazy Load concept. It uses Thread Pool to download images in parallel in worker threads . It also uses LruCache provided by Android to store/load images without any extra efforts for memory management. ImageLoader Class handles the Images to be displayed in ListView, Load images from cache or queue them to be download from web server (load from external storage).
  2. MemoryCache – It  contains a LruCache object. LRUCache is a caching strategy where we use Least Recently Used (LRU) eviction policy when the cache is full and we want to add more new data to the cache. LruCache is given fixed size (Either number of items or size in kilobytes) to store Bitmaps. When we add more Bitmaps the least recently used Bitmaps will become candidates for garbage collection. LRUCache cache was introduced in API level 12 (3.1) but is available through the Support Library back to 1.6.
  3. FileCache – Used to create folder in external storage (or sdcard) and also clear images from file when  specified memory limit reached.
  4. ImageLoaderRunnable – This is for processing the request to load the image from local device storage or download the image from server
  5. BitmapDisplayerRunnable – This is for placing the image in the ImageView.

Sequence Diagrams:

Following Sequence Diagram depicts the whole life-cycle:

1. Sequence Diagram – Accessing image from Cache and putting a request in Queue

InnovationM - Sequence Diagram Lazy Loading Images In Android

2. Sequence Diagram – Processing request from Queue

InnovationM - Sequence Diagram Lazy Loading Images In Android

3. Sequence Diagram – Placing Image for display

InnovationM - Sequence Diagram Lazy Loading Images In Android Code Snippets:

public class ImageLoader 
{
	MemoryCache memoryCache;
	FileCache fileCache;
	Resources resources;
	private static final int DEFAULT_NO_OF_THREADS = 5;
	private final int imagePlaceHolderId;
	private Map<ImageView, String> recyclerMap = Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
	ExecutorService executorService;
	Handler handler = new Handler();
	private Map<String, Boolean> serverRequestMap  = Collections.synchronizedMap(new HashMap<String, Boolean>());

	public ImageLoader(Context context, int imagePlaceHolderId, int lruCacheSize, int lruCacheUnit, 
			int maxFileCacheSize, String fileCacheFolder, int numberOfThreads)
	{
		this.imagePlaceHolderId = imagePlaceHolderId;
		this.resources = context.getResources();

		if(numberOfThreads < 1)
		{
			numberOfThreads = DEFAULT_NO_OF_THREADS;
		}

		memoryCache = new MemoryCache(lruCacheSize, lruCacheUnit);
		fileCache = new FileCache(context, maxFileCacheSize, fileCacheFolder);
		executorService = Executors.newFixedThreadPool(numberOfThreads);
	}

	public void displayImage(String imageName, String imageUrl, ImageView imageView)
	{
		recyclerMap.put(imageView, imageName);
		Bitmap bitmap = memoryCache.getImageFromCache(imageName);

		if(bitmap!=null)
		{
			imageView.setImageBitmap(bitmap);
		}
		else
		{
			queuePhoto(imageName, imageUrl, imageView);

			try
			{
				imageView.setImageDrawable(resources.getDrawable(imagePlaceHolderId));
			}
			catch(NotFoundException notFoundException)
			{
				throw notFoundException;
			}	
		}
	}

	private void queuePhoto(String imageName, String imageUrl, ImageView imageView)
	{
		ImageInfo imageInfo = new ImageInfo(imageName, imageUrl, imageView);
		executorService.submit(new ImageLoaderRunnable(imageInfo));
	}

	boolean imageViewReused(ImageInfo photoToLoad)
	{
		String imageName = recyclerMap.get(photoToLoad.getImageView());
		if(imageName==null || !imageName.equals(photoToLoad.getImageName()))
		{
			return true;
		}

		return false;
	}
}

 

class ImageLoaderRunnable implements Runnable 
{
	ImageInfo photoToLoad;
	ImageLoaderRunnable(ImageInfo photoToLoad)
	{
		this.photoToLoad = photoToLoad;
	}

	public void run() 
	{
		try
		{
			if(imageViewReused(photoToLoad))
			{ 
				return;
			}

			String imageName = photoToLoad.getImageName();
			String imageUrl = photoToLoad.getImageUrl();
			Bitmap bitmap = null;
			Boolean isServerRequestExists = serverRequestMap.get(imageName);
			isServerRequestExists = (isServerRequestExists == null ? false : isServerRequestExists);

			if(!isServerRequestExists) //If Server request not exists, take hit
			{
				serverRequestMap.put(imageName, true);

				try
				{
					bitmap = getImage(imageName, imageUrl);
				}
				catch(Exception exception)
				{
					serverRequestMap.put(imageName, false);
					if(imageViewReused(photoToLoad))
					{ 
						return;
					}

					bitmap = getImage(imageName, imageUrl);
					serverRequestMap.put(imageName, true);
				}
			}
			else
			{
				return;
			}

			if(bitmap != null)
			{
				serverRequestMap.remove(photoToLoad.getImageName());
				memoryCache.saveImageToCache(imageName, bitmap);
			}

			if(imageViewReused(photoToLoad))
			{ 
				return;
			}

			BitmapDisplayerRunnable bitmapDisplayer = new BitmapDisplayerRunnable(bitmap, photoToLoad);

			handler.post(bitmapDisplayer);
		}
		catch(Throwable throwable)
		{
			throwable.printStackTrace();
		}
	}

	public Bitmap getImage(String imageName, String url) throws ClientProtocolException, FileNotFoundException, InnovationMException, IOException
	{
		File file = null;
		Bitmap bitmap = null;
		file = fileCache.getFileFromFileCache(imageName);

		try
		{
			bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
		}
		catch(FileNotFoundException fileNotFoundException)
		{
			//Consume
		}

		if(bitmap != null)
		{
			return bitmap;
		}
		else
		{
			if(AppUtil.isImageUrlValid(url))
			{
				//Download From WS
				bitmap = ImageManager.downloadBitmap(url, file);
			}					
		}

		return bitmap;
	}

}

 

class BitmapDisplayerRunnable implements Runnable
	{
		Bitmap bitmap;
		ImageInfo photoToLoad;
		public BitmapDisplayerRunnable(Bitmap bitmap, ImageInfo imageInfo)
		{
			this.bitmap = bitmap;
			this.photoToLoad = imageInfo;
		}

		public void run()
		{
			if(imageViewReused(photoToLoad))
			{
				return;
			}

			if(bitmap!=null)
			{
				photoToLoad.getImageView().setImageBitmap(bitmap);
			}
			else
			{
				photoToLoad.getImageView().setImageResource(imagePlaceHolderId);
			}
		}
	}

 

 

Leave a Reply