Some things in Android are easy to do, most things are not. It seems like to create an app you can easily get 60-70% of the way to where you want to be, but going the extra 30% to create a really polished interface and user experience is a heck of a lot of work. I guess this is true in most systems, but fighting the Android API and documentation really drives the point home.
In the app I’m working on I want to display a grid of images. That’s not a wholly unreasonable request, one simply has to retrieve the list of images from the SD card, thumbnail them, and display them to the user. Of course, once you decide the images need to load fast, and appear in a certain order, and realize the user has 500+ images in their Camera album while their phone barely has enough memory or processing power life becomes complicated.
How does one achieve all these design goals? Caching is the obvious answer. Load all the images into a cache, and display as needed the prerendered thumbnails. However, we’re working on a resource-constrained system, memory is precious and I’m tired of seeing applications in Android take up 10s of MB holding onto large numbers of images. I’ve seen the Facebook app do it, as well as countless others. It shouldn’t be that hard to build an image cache in memory with a reasonable eviction policy, right? I want to keep just as many images as needed, without causing OutOfMemory
exceptions, and provide machinery that automatically refreshes images as needed when they have been evicted.
Approach One: Using SoftReference
Reading on StackOverflow and Android development blogs I saw a lot of people recommending Java’s built-in SoftReference
type to wrap the Bitmap resources. The idea behind the soft reference is that the Java VM will keep them around, even in the face of garbage collection, as long as there’s enough memory around to do what it needs to do. That seems reasonable, so I went with it and coded up a class implementing this idea:
private class BitmapCacher { private ImageView iv; private SoftReference<Bitmap> res; BitmapCacher() { Bitmap bitmap = getImage(); if (bitmap != null) { res = new SoftReference<Bitmap>(bitmap); } } private Bitmap getImage() { Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail( SelectPhotosActivity.this.getContentResolver(), imageId, MediaStore.Video.Thumbnails.MICRO_KIND, null); if (bitmap != null) { Bitmap newBitmap = Bitmap.createScaledBitmap(bitmap, thumbnailWidth, thumbnailWidth, true); bitmap.recycle(); return newBitmap; } return null; } public void bindBitmap(ImageView iv) { this.iv = iv; if (res == null || res.get() == null) { (new BitmapGetter()).execute((Object) null); } else { iv.setImageBitmap(res.get()); } } private class BitmapGetter extends AsyncTask<Object, Object, Bitmap> { @Override protected Bitmap doInBackground(Object... params) { return getImage(); } protected void onPostExecute(Bitmap result) { iv.setImageBitmap(result); res = new SoftReference<Bitmap>(result); } } }
Looks decent, we hold an internal reference to the SoftReference
bitmap, to prevent too many hard references from leaking out. We create a new function called bindBitmap
that attaches the bitmap to an ImageView
. This function follows the typical design pattern of optimizing the common case, and ensuring the odd case completes. If the image is in the cache, as we’d hope, then we simply load the image synchronously, if it’s not we fire off an AsyncTask
worker to load it and update the ImageView
on completion on the UI thread.
The SoftReference
should hold on to the bitmap as long as is practical, and then evict it.
So unfortunately all those recommending SoftReferences
have never actually tried using them, they don’t work at all. Immediately upon loading the bitmap into the SoftReference
it’s destroyed at the next garbage collection. Why would a SoftReference
do this?
It turns out in Android SoftReference
s are implemented a little differently than one would assume. Check out the explanation on this bug request. Basically what the dev team has decided is that in Android a SoftReference
doesn’t really make sense. Your app is competing with all the other apps in memory, and due to the constrained nature of the platform it’s actually hard for the VM to decide when to destroy your SoftReference
. There’s no way for it to know that your image cache is more important than the other applications in memory, so the team has just made SoftReference
s basically ineffective. I’d have to agree with them, they only make sense when your understanding of the problem is insufficient.
So that’s great, where to next?
Approach Two: Using a LRUCache
The recommended approach is to use an LRUCache (least recently used) object to store images. The big difference here is that instead of using an object that’s competing with everything else on the phone for resources, and relying on the VM to magically know how to prioritize, we instead declare a fixed-size cache. We’re explicitly saying how many images we’d like to keep in memory, instead of using SoftReferences
and wanting to store an unbounded number, kept in check by the phone’s total resource limits.
The LRUCache isn’t available in API level 10, which is what most people are developing for, but luckily it’s been ported into the Android Support Package, which is easily installed by following these instructions.
The implementation of my Bitmap cache changed quite a bit while I was figuring it out, but here’s the really important bits:
Declaration of the LRUCache
private LruCache<Integer, Bitmap> mCache; ImageLoader(ImageLoadListener lListener, Context context){ mCache = new LruCache<Integer, Bitmap>(4*1024*1024) { protected int sizeOf(Integer key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; }
We set the cache size to 4MiB. Typically the LRUCache uses the count of images to determine eviction, but when dealing with Bitmaps it makes a little more sense to use the actual byte-size of the image. In newer versions of the Android API there’s a nice getByteCount()
method to retrieve the byte size. In API 10 we have to resort to a hackish multiplication.
Filling and Retrieving from the Cache
Bitmap lBitmap; synchronized(mCache) { lBitmap = mCache.get(imageId); } if(lBitmap == null) { lBitmap = getImage(imageId, imageSize); synchronized(mCache) { mCache.put(imageId, lBitmap); } }
Filling the cache requires some leg-work, the hardest part is to ensure we’re doing so atomically. Locking on the cache works nicely, and will prevent corruption of the internal state of the object.
Performance
Using the LRUCache has really sped up my application. I’m loading images asynchronously into a GridView, and by using the LRUCache I can preserve a smooth UI scroll once loading has finished.
This approach has some really nice properties to it. My cache is sized to hold what I think is a ‘typical’ number of image thumbnails found on a user’s device, with a little bit of padding. We get great performance when the number of photos on the device is below this threshold, so the common case is optimized. We sacrifice some performance when the user has a large number of images, there will be some jerk to the UI as it scrolls past images that were recently viewed, but we perserve functionality, which is the best we can hope for.