Wednesday, March 16, 2011

How to Build an Image Cache Manager in Android

In mobile phone networks, http connections are expensive and time-consuming. Fetching images is a very common job that is in almost every application. Other than asynchronous loading, it is better to cache the images for future reuse.

Reasons for caching images:
  • avoid http-fetching of the same images; faster loading of the activity
  • better memory managing (if you are showing same images in a list)

Concept


We will store the in-memory images in a HashMap that maps a filename to an ImageCacheObject. The ImageCacheObject will have the bitmap image and a the lastRetrieved date (the date when the image was last retrieved). We will have an ImageCacheManager that deletes the images that were not used in a period of time from the HashMap. This ImageCacheManager will run on a timer thread.


Implementation

Let's assume you already have the following functions:
  • fetchImage(String fileUrl, Activity a, boolean useImageCache) - the activity is used for retrieval of the Application object

We will go through the implementation of these image cache components
  • ImageCacheObject - holds the image and the lastRetrieved date (when the image was last used)
  • ImageCache - a subclass for HashMap that maps the filename (key) to the image
  • ImageCacheManager - timer for deleting images that are not being used
  • CustomApplication - extends android.app.Application and manages the ImageCacheObject to expire the older images; you should name this to your application's name

ImageCacheObject

There are only the bitmap and lastRetrieved date in this object. This is used by the ImageCache Hashmap.


package com.custom.android;

import java.util.Date;

import android.graphics.Bitmap;

public class ImageCacheObject {

public Bitmap bm;
public Date lastRetrieved;
public ImageCacheObject(Bitmap bm) {
lastRetrieved = new Date();
this.bm = bm;
}
public void touch() {
lastRetrieved = new Date();
}
public Bitmap getBitmap() {
touch();
return bm;
}
}


ImageCache

empty() is for clearing out the whole ImageCache HashMap, while expireOldCache is for removing the images that were not retrieved in one min (60000ms). You may want to make this time interval dynamic. A user that browses slowly will populate the ImageCache much slower than a user who browses quickly; in this case, you will want a longer timeout before each image is deleted. You may also want to consider memory use as well.


package com.custom.android;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import android.graphics.Bitmap;
import android.util.Log;

public class ImageCache extends HashMap<String, ImageCacheObject> {

public void addImage(String filename, Bitmap image) {
synchronized (this) {
if (!this.containsKey(filename))
this.put(filename, new ImageCacheObject(image));
}
}

public Bitmap getImage(String filename) {
synchronized (this) {
if (this.size() > 0) {
ImageCacheObject o = this.get(filename);
return (o != null) ? o.getBitmap() : null;
}
}
return null;
}

public void empty() {
empty(false);
}
public void empty(boolean doTimeCheck) {

Date d = new Date();

synchronized (this) {
if (this.size() > 0) {

try {

ArrayList<String> list = new ArrayList<String>(); 
for (Iterator i = this.entrySet().iterator(); i.hasNext();) {

Map.Entry<String, ImageCacheObject> pair = (Map.Entry<String, ImageCacheObject>) i
.next();
ImageCacheObject o = pair.getValue();

if (!doTimeCheck || d.getTime() - o.lastRetrieved.getTime() > 60000) {
o.bm = null;
pair.setValue(null);
list.add(pair.getKey());
}

}

for(int i = 0; i < list.size(); i++) {
this.remove(list.get(i));
}
} catch (Exception e) {
Log.v("ImageCache", "ImageCache: Exception in function empty()");
}
}
}

}

public void expireOldCache() {
empty(true);
}
}


ImageCacheManager

This is used for periodically clearing out the ImageCache hashmap. isRunning prevents multiple TimerTask from running at the same time.



package com.custom.android;

import java.util.TimerTask;

public class ImageCacheManager extends TimerTask {

private boolean isRunning = false;

public void run() {
if (isRunning)
return;

synchronized (this) {
isRunning = true;

try {

CustomApplication.mApp.expireOldCache();
} catch (Exception e) {

} catch (Throwable t) {

}
isRunning = false;
}

}

}



CustomApplication

This is where any activities can access the ImageCache hashmap.


package com.custom.android;

import java.util.Timer;

import android.app.Activity;
import android.app.Application;
import android.graphics.Bitmap;

public class CustomApplication extends Application {

public static CustomApplication mApp = null;
private ImageCache mImageCache = new ImageCache();
static private Timer cacheTimer = new Timer();

@Override
public void onCreate() {
mApp = this;
cacheTimer.schedule(new ImageCacheManager(), 0, 30000);
super.onCreate();
}

@Override
public void onTerminate() {
cacheTimer.cancel();
cacheTimer = null;
super.onTerminate();
}

public static CustomApplication getCustomApplication(Activity a) {
Application app = a.getApplication();
return (CustomApplication) app;
}
public Bitmap getImage(String filename) {
return mImageCache.getImage(filename);
}

public void addImage(String filename, Bitmap image) {
mImageCache.addImage(filename, image);
}

public void expireOldCache() {
mImageCache.expireOldCache();
}

}


FetchImage

This will be your utility function for fetching images from a http connection. I provided a sample fetch function below. The red-colored text are places where you want to put the code. The function will try to get the image from the ImageCache if it exists.  If not, it will add the image to the cache.

public static Bitmap fetchImage(String fileUrl, Activity a, boolean useImageCache) {

if (fileUrl == null || fileUrl.equals(""))
return null;

Bitmap bm = null;

if (a != null) {
CustomApplication pa = CustomApplication.getCustomApplication(a);

bm = pa.getImage(fileUrl);

if (bm != null) {
return bm;
}
}

Bitmap image = null;
URL myFileUrl = null;
HttpURLConnection conn = null;

try {
myFileUrl = new URL(fileUrl);
} catch (MalformedURLException e) {
return null;
}

try {
CustomApplication pa = CustomApplication .getCustomApplication(a);
conn = (HttpURLConnection) myFileUrl.openConnection();
conn.setDoInput(true);
conn.connect();

InputStream is = conn.getInputStream();
image = BitmapFactory.decodeStream(is);

conn.disconnect();
conn = null;
if (image != null) {

if (useImageCache && image != null) {
CustomApplication.getCustomApplication(a).addImage(fileUrl, image);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {

if (conn != null)
conn.disconnect();
}

return image;
}


Note:


When dealing with a lot high quality images, you can get a OutOfMemoryError. Be sure to deal with memory allocation when the images are no longer used.  Use image.recycle() or call System.gc() explicitly to free memory.