ImageView / WeakReference

Memory Cache – Part 3

In the previous part of this series we looked at why caching images may help the performance of our app, so let’s firstly look at a technique for creating a short-lived cache. The items in the cache will only survive until the next GC if they are not strongly referenced, but this can sometimes be desirable if we want to be extremely conservative with resources.

Let’s create a new class called MemoryCache which will be out cache implementation. For now we’ll just populate it with a constructor and a utility method to load an image from the assets folder that we’ll use throughout:

[java] public class MemoryCache
{
private final Map> cache =
new HashMap>();
private final Context context;

public MemoryCache( Context context )
{
this.context = context.getApplicationContext();
}

@Override
public String toString()
{
return “Cache size: ” + cache.size();
}

public Bitmap getImage( String assetName )
{
WeakReference ref = cache.get( assetName );
Bitmap bitmap = ref == null ? null : ref.get();
if (bitmap == null)
{
bitmap = Utils.loadAsset( context, assetName );
if (bitmap != null)
{
cache.put( assetName,
new WeakReference( bitmap ) );
}
}
return bitmap;
}

public boolean isCached( String assetName )
{
refreshCache();
return cache.containsKey( assetName );
}

public void refreshCache()
{
List removals = new LinkedList();
for (String key : cache.keySet())
{
WeakReference bm = cache.get( key );
if (bm.get() == null)
{
removals.add( key );
}
}
for (String key : removals)
{
cache.remove( key );
}
}
}
[/java]

First we have are defining our cache as a Map of WeakReference objects keyed on a String identifier. We have overridden toString() to give us the cache size, and finally implemented getImage() which is the workhorse of our cache. This check for the existence of an entry in the cache and verifies that the weak reference still refers to an object. If not, then we load an new image using our utility method, and store a weak reference to it in the cache. There are also a couple of other methods, one which allows us to check whether there’s a cached item for a given key, and the other performs cleanup of orphaned keys (i.e. keys for a weak reference to an object which has been GC’d).

If we now modify our MemoryCacheActivity:

[java] @Override
public void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
setContentView( R.layout.main );

imageView1 = (ImageView) findViewById( R.id.imageView1 );
imageView2 = (ImageView) findViewById( R.id.imageView2 );
memCache = new MemoryCache( getApplicationContext() );

loadManual();
Log.d( TAG, memCache.toString() );
loadCached();
Log.d( TAG, memCache.toString() );
loadManual();
Log.d( TAG, “Cached: ” + memCache.isCached( ASSET_NAME ) );
Log.d( TAG, memCache.toString() );
System.gc();
Log.d( TAG, “GC” );
Log.d( TAG, “Cached: ” + memCache.isCached( ASSET_NAME ) );
Log.d( TAG, memCache.toString() );
}

private void loadCached()
{
TimingLogger tl = new TimingLogger( TAG, “Cached image loading” );
imageView1.setImageBitmap( memCache.getImage( ASSET_NAME ) );
tl.addSplit( “first” );
imageView2.setImageBitmap( memCache.getImage( ASSET_NAME ) );
tl.addSplit( “second” );
tl.dumpToLog();
}
[/java]

Our loadCached() method is very similar to the loadManual() method except that we use getImage() rather than calling our utility method directly. In out onCreate() we first call loadManual(), as before. Then we call loadCached() to load our ImageViews with bitmaps from the cache, then we call loadManual() again which will remove the string references to the image in the cache which is from the ImageView object. Then we dump the status of the cache either side of a manual GC call. When we run this we see the following in the log:

[code] Standard image loading: begin
Standard image loading: 103 ms, first
Standard image loading: 94 ms, second
Standard image loading: end, 197 ms
Cache size: 0
Cached image loading: begin
Cached image loading: 95 ms, first
Cached image loading: 0 ms, second
Cached image loading: end, 95 ms
Cache size: 1
Standard image loading: begin
Standard image loading: 97 ms, first
Standard image loading: 87 ms, second
Standard image loading: end, 184 ms
Cached: true
Cache size: 1
GC
Cached: true
Cache size: 0
[/code]

So the initial manual load takes around 200ms, as before. Next the cached image loading takes around half that time because the image is loaded for the first ImageView and cached, and the second one uses the cached value so there is no loading time. We can now see (line 10) that the cache contains one item. Next we perform a manual load again which removes any strong references to the image in the cache. But the cache status is still showing that it contains one item (line 15). This is because although the cached image no longer has any strong references, GC hasn’t yet run and so the image still exists in memory. After we manually perform a GC we see that the cache no contains no items.

So we can see that when the memory cache is in use, we half the time taken to load the same image twice, and this will increase further as we use the images subsequent times.

Now for the important bit: Our MemoryCache implementation isn’t actually storing references to bitmaps it is simply giving us a mechanism to reuse a Bitmap object if it is already strongly referenced from elsewhere. Specifically, when we use ImageView.setImageBitmap(), the ImageView will hold a reference to our Bitmap object, and it won’t be GC’d. This is the use-case where this technique is most useful, where we want to re-use images if they are already in use and referenced elsewhere. However, if they are not referenced, then they will be GC’d.

While this can certainly improve performance when we are reusing the same image in a particular Activity and it gives the illusion of being a cache, in reality it is not. Unreferenced images will be reclaimed by the JVM during the next GC and intensive operations such as switching Activities, or even loading another bitmap are likely to trigger a GC. A true cache would hold the images for longer than the next GC, and in the next part of this series we’ll look at a true cache implementation.

I should include a couple of caveats. Firstly, the MemoryCache implementation here has been kept simple so as to keep the memory cache concept as clear as possible. However, the code as-is will probably not work that well in a production app because it not coded to be thread safe. For example while refreshCache is running, a call is made on getImage for an image which is not yet cached. This will most likely result in a ConcurrentModificationException. You should be aware of this before simply using the code as-is.

Secondly, calling refreshCache() from isCached() for periodically clearing out orphaned keys is hacky, to say the last. I would urge you to periodically call refreshCache() from within getImage() (say once every 100 times getImage() is called) to prevent you filling up memory with orphaned keys in the cache which would rather defeat the object! I haven’t done this in the example to keep it as simple, readable, and understandable as possible.

It is also worth mentioning that if you are reliant on any kind of cache for your images (or indeed have two references to the same bitmap), you should refrain from calling recyle() on your bitmap images. recycle() is useful when you want to immediately free up the resources used by a Bitmap object but will cause you major problems if you are sharing your bitmaps!

If you look at the Java docs for WeakReference, you may spot WeakHashMap and wonder if that could be used top make things even simpler still. The simple anwer to this is: no, it can’t. The reason for this is that it is the keys that are weakly referenced in WeakHashMap, and not the values. For our implementation we require that the values are weakly refernced and not the keys.

The source for this article can be found here.

© 2012, Mark Allison. All rights reserved.

Copyright © 2012 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.