of the challenges of developing for mobile devices is making your app run fast, even though it only has very limited capabilities. This is the problem we’ll be adressing in this post, or to be more exact, we’ll be adressing how to get your images fast and efficient on a mobile device. Now obviously you can’t get your image faster than the device’s connection allows, but you can make sure you’re not waiting for downloads that you don’t have to wait for. This is where the image cache comes in. If you have an image, why not just reuse it when requesting it for the second time? Of course there are situations where this sort of behaviour is not wanted (for example, when the image on the server has changed), but mostly it will save you time, bandwidth and battery life.
So what should you take into account when implementing an image cache? Well, first of all, we want to store the images locally beyond the runtime of our cache. So we’ll to store them on the phone. We do this because most apps only run for a few minutes, meaning that the user will probably only request the image once (making our cache only part of the problem!). Secondly we want to store them in memory, for faster access on the second call. Basically, we want to download an image once, store it in memory and on the phone and the next time the image is requested we’ll just get it from the local file into memory and we’re done. Now we could implement a background thread to refresh the image too, giving the cached image first and later returning the newly loaded image. Personally, though, I prefer to use a timeout, simply because it prevents us from having to download the image everty time anyway, and that’s exactly what we’re trying to prevent :p
I should note that the image cache used in this tutorial is focussed on the Android 1.6 platform, but it can be easily converted into any other java platform by replacing the Drawable class with any other image class. Also, at some points I’ll go into some really minor details, so if you don’t feel like reading the whole thing don’t be afraid to skip a piece here and there
The ImageCache class will obviously be a final singleton class, to make it accessible from all places. We’ll use hashmaps to store the image data with the hashed url as the key, this way we’ll be able to easily access the cached data using the url passed by the request. As said earlier, we’ll also be storing the images on the phone, using the same hash as the file name, this “naming convention” is mostly for ease of use (as you’ll see later).
Synchronization is very important when writing any kind of cache, as we obviously want it to be thread safe and we’ll be doing some asyncronous loading ourselves to make matters even worse . There are two things we need to synchronize: requests for the same url and access to the cache hashmap.
And now for the code. I’ll be giving you the full code first and then go into the juicy details.
package utils;import android.content.Context;import android.graphics.drawable.Drawable;import android.util.Log;import java.io.File;import java.io.FileOutputStream;import java.io.InputStream;import java.lang.ref.WeakReference;import java.math.BigInteger;import java.net.URL;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.util.ArrayList;import java.util.Date;import java.util.HashMap;import java.util.List;import java.util.Random;/** * The ImageCache class can be used to download images and store them in the * cache directory of the device. Multiple requests to the same URL will result * in a single download, until the cache timeout has passed. * @author Thomas Vervest */public class ImageCache { /** * The ImageCallback interface defines a single method used to pass an image * back to the calling object when it has been loaded. */ public static interface ImageCallback { /** * The onImageLoaded method is called by the ImageCache when an image * has been loaded. * @param image The requested image in the form of a Drawable object. * @param url The originally requested URL */ void onImageLoaded(Drawable image, String url); } private static ImageCache _instance = null; /** * Gets the singleton instance of the ImageCache. * @return The ImageCache. */ public synchronized static ImageCache getInstance() { if (_instance == null) { _instance = new ImageCache(); } return _instance; } private static final long CACHE_TIMEOUT = 60000; private final Object _lock = new Object(); private HashMap<String, WeakReference<Drawable>> _cache; private HashMap<String, List<ImageCallback>> _callbacks; private ImageCache() { _cache = new HashMap<String, WeakReference<Drawable>>(); _callbacks = new HashMap<String, List<ImageCallback>>(); } private String getHash(String url) { try { MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(url.getBytes()); return new BigInteger(digest.digest()).toString(16); } catch (NoSuchAlgorithmException ex) { // this should never happen, but just to make sure return the url return url; } } private Drawable drawableFromCache(String url, String hash) { Drawable d = null; synchronized (_lock) { if (_cache.containsKey(hash)) { WeakReference ref = _cache.get(hash); if ( ref != null ) { d = ref.get(); if (d == null) _cache.remove(hash); } } } return d; } private Drawable loadSync(String url, String hash, Context context) { Drawable d = null; try { d = drawableFromCache(url); File f = new File(context.getCacheDir(), hash); boolean timeout = f.lastModified() + CACHE_TIMEOUT < new Date().getTime(); if (d == null || timeout) { if (timeout) f.delete(); if (!f.exists()) { InputStream is = new URL(url + "?rand=" + new Random().nextInt()).openConnection().getInputStream(); if (f.createNewFile()) { FileOutputStream fo = new FileOutputStream(f); byte[] buffer = new byte[256]; int size; while ((size = is.read(buffer)) > 0) { fo.write(buffer, 0, size); } fo.flush(); fo.close(); } } d = Drawable.createFromPath(f.getAbsolutePath()); synchronized (_lock) { _cache.put(hash, new WeakReference(d)); } } } catch (Exception ex) { Log.e(getClass().getName(), ex.getMessage(), ex); } return d; } /** * Loads an image from the passed URL and calls the callback method when * the image is done loading. * @param url The URL of the target image. * @param callback A ImageCallback object to pass the loaded image. If null, * the image will only be pre-loaded into the cache. * @param context The context of the new Drawable image. */ public void loadAsync(final String url, final ImageCallback callback, final Context context) { final String hash = getHash(url); synchronized (_lock) { List callbacks = _callbacks.get(hash); if (callbacks != null) { if (callback != null) callbacks.add(callback); return; } callbacks = new ArrayList(); if (callback != null) callbacks.add(callback); _callbacks.put(hash, callbacks); } new Thread(new Runnable() { @Override public void run() { Drawable d = loadSync(url, context); List callbacks; synchronized (_lock) { callbacks = _callbacks.remove(hash); } for (ImageCallback iter : callbacks) { iter.onImageLoaded(d, url); } } }, "ImageCache loader: " + url).start(); }}
I presume you know the basics of the singleton pattern, so I’ll skip the details on that and begin with the variables definitions (see snippet below). The static and final CACHE_TIMEOUT variable kinda spreaks for itself, it defines the maximum time an image is allowed to remain cached before it has to be downloaded from the server again. This time is in milliseconds, so 60000 means images in the cache will be invalid after a minute.
We’ll be using the _lock object to synchronize all our actions. We use a final Object for this because we will be accessing the cache from different scopes (both ImageCache internal as well as inline Runnable objects), so we need a consistent lock object (not just “this”).
Next we have the actual drawable cache. We’re using a basic HashMap with a String key and a WeakReference object referencing the actual Drawable. The WeakReference object keeps the Drawable object open for garbage collection, so the JVM will be able to remove unused Drawables from our cache if nessecary. This won’t bother us (as you’ll see later on) because when a Drawable is GC’d we’ll just grab the local file and recreate it. This way we’ll have fast access to our Drawable and at the same time prevents huge chunks of memory being reserved for nothing, while the phone is running out of memory.
Last but not least is a HashMap containing a String key and a List with ImageCallback objects. This is our main means of transferring a loaded image back to the requesting object. The reason why this is a List (and not just a single ImageCallback object) is because we don’t want to request the same image multiple times at the same time (remember the synchronization points?). Basically we do the acutal request once and if another request is done while the image is downloading we’ll just add the request to the list of ImageCallback objects for that image. This way if we request an image multiple times, we’ll be able to hand the image back as soon as the first download finishes. How’s that for efficient!
private static final long CACHE_TIMEOUT = 60000;private final Object _lock = new Object();private HashMap> _cache;private HashMap> _callbacks;
The constructor is private because this is after all a singleton class and we don’t want anybody else making a second instance. Other than that it’s nothing special, we just initialize the cache and callbacks hashmaps.
private ImageCache() { _cache = new HashMap<String, WeakReference<Drawable>>(); _callbacks = new HashMap<String, List<ImageCallback>>();}
Now for the first trick we have up our sleeve: the url hash. We take the url and make a MD5 hash of it, and this is our HashMap key. Okey, so it isn’t really such a special trick, but it gets the job done. Oh, and just in case the MD5 digest isn’t known, we’ll just return the url. Less elegant, but it gets the job done just as well (although you really would want to think of another way of generating a hash if you discover this to be true…).
private String getHash(String url) { try { MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(url.getBytes()); return new BigInteger(digest.digest()).toString(16); } catch (NoSuchAlgorithmException ex) { // this should never happen, but just to make sure return the url return url; }}
The next method does all the Drawable retreiving work. First we get the hash of the url. Next we enter a synchronization block to prevent the image loading thread from modifying the cache while we’re reading from it. If the cache contains the hash key we get the WeakReference object from it and check if the reference is still valid and if the reference has been GC’d we’ll remove the item from the cache. Finally we return the Drawable object or null.
private Drawable drawableFromCache(String url, String hash) { Drawable d = null; synchronized (_lock) { if (_cache.containsKey(hash)) { WeakReference ref = _cache.get(hash); if ( ref != null ) { d = ref.get(); if (d == null) _cache.remove(hash); } } } return d;}
Next we’ll be discussing the workhorse of the ImageCache class, the loadSync method. This method loads the image from the server and adds it to the cache HashMap. We also pass a Context object to the method so we can get the cache directory of the current Context.
First we try to get the Drawable form the cache, obviously there is no use in downloading an image we already have (that’s the point of the cache, remember?). Right, so next up is getting the file from the cache directory. As described earlier, the name of the files is the hash, so we’ll be looking for that file. Next we check if the timeout has passed and the file should be deleted and downloaded again from the server. We can see when we last updated the file by checking the last modification date, and if the file does not exist the last modified value of the will be 0, so all will be fine.
If we couldn’t retreive the image from memory we’ll try to load it from the local file. If the cache expired we’ll delete the local file, forcing the cache to download it from the server. Next we create a drawable from the file and add it to the cache HashMap (and, as usual, we do this in a synchronized block). Finally we return the new drawable, or null on error.
private Drawable loadSync(String url, String hash, Context context) { Drawable d = null; try { d = drawableFromCache(url); File f = new File(context.getCacheDir(), hash); boolean timeout = f.lastModified() + CACHE_TIMEOUT >= new Date().getTime(); if (d == null || timeout) { if (timeout) f.delete(); if (!f.exists()) { InputStream is = new URL(url + "?rand=" + new Random().nextInt()).openConnection().getInputStream(); if (f.createNewFile()) { FileOutputStream fo = new FileOutputStream(f); byte[] buffer = new byte[256]; int size; while ((size = is.read(buffer)) > 0) { fo.write(buffer, 0, size); } fo.flush(); fo.close(); } } d = Drawable.createFromPath(f.getAbsolutePath()); synchronized (_lock) { _cache.put(hash, new WeakReference(d)); } } } catch (Exception ex) { Log.e(getClass().getName(), ex.getMessage(), ex); } return d;}
The loadAsync method is the only public method of the ImageCache class. It makes sure no redundant downloads are executed and calls the callbacks if either the Drawable is retreived from the cache or downloaded form the server. First of all we get the hash of the url. This variable is final because we’ll be reusing it in the downloading thread. That is, if we haven’t cached it already ofcourse.
The next part is wrapped in a synchronized block, again to prevent simultanious reading and writing of the cache, but also to prevent simultainous access to the callback lists. What we do here is check if there are any ImageCallback objects registered with the hash key. We know that a download is in progress if there already is a callback registered. In this situation we’ll just add the new callback to the list and stop. If no callbacks are registered for the hash we’ll set a new list to the hash key, containing the new callback.
Next we’ll start a new thread to download the image using the loadSync method. The loadSync method returns the new Drawable, and all we have to do next is pass it to all the callbacks registered for it. We get the list of registered callbacks from the callbacks HashMap. Of course we do this in a synchronization block, for the usual reasons, and we remove the list from the HashMap to prevent us from invoking the same callback twice. Now all that’s left to do is invoke all the callbacks in the list and we’re done!
public void loadAsync(final String url, final ImageCallback callback, final Context context) { final String hash = getHash(url); synchronized (_lock) { List callbacks = _callbacks.get(hash); if (callbacks != null) { if (callback != null) callbacks.add(callback); return; } callbacks = new ArrayList(); if (callback != null) callbacks.add(callback); _callbacks.put(hash, callbacks); } new Thread(new Runnable() { @Override public void run() { Drawable d = loadSync(url, context); List callbacks; synchronized (_lock) { callbacks = _callbacks.remove(hash); } for (ImageCallback iter : callbacks) { iter.onImageLoaded(d, url); } } }, "ImageCache loader: " + url).start();}
And that’s the whole lot! There are some minor points in the class that can be improved. If a timeout occurs on an image the imagecache could return the old image before the new image has loaded. This way the user sees the old image and as soon as the new image has been loaded the image refreshes to the updated version. It isn’t a big improvement, but it could be a really cool feature