Loading, caching and displaying images in Android (Part 1)

Loading, caching and displaying images in Android (Part 1)

Displaying an image in a mobile application is one of the most common tasks for app developers. Nearly every application displays some kind of graphics. Surprisingly, it can be quite challenging to efficiently load and display an image on Android.

Part 1 of this 2 part series will describe several established patterns to load, cache and display images, while at the same time avoiding certain pitfalls that diminish your app’s user experience. We will not provide you with a snippet collection but rather a conceptual walk through.

For the purpose of this post we will assume that the images we want to display are located on a remote HTTP server.

Concurrent loading with the AsyncTask

Loading via a network connection means that we are dealing with a long running operation, which we don’t want to execute on the main UI thread. Android offers the AsyncTask class, that takes most of the work from our shoulders by loading the data in a separate background thread and then publishing the result on the UI thread:

public class ImageLoader extends AsyncTask {

  private final WeakReference viewReference;

  public ImageLoader( ImageView view ) {
    viewReference = new WeakReference( view );
  }

  @Override
  protected Bitmap doInBackground( String... params ) {
    return loadBitmap( params[ 0 ] );
  }

  @Override
  protected void onPostExecute( Bitmap bitmap ) {
    ImageView imageView = viewReference.get();
    if( imageView != null ) {
      imageView.setImageBitmap( bitmap );
    }
  }

  ...

}

Note how we use a WeakReference to hold onto the ImageView. When the user shuts down the application or simply rotates the device the current Activity is closed and/or restarted. The previous Activity can not be disposed of while we are still holding references to it, so we are facing a potential memory leak. The most common solution to this problem is to use a WeakReference when holding onto the parent Activity. An object referenced by a weak reference can still be garbage collected when no other soft or weak reference points to it. Once we have lost the reference to the previous Activity, it is the responsibility of the new parent Activity to re-inject the target View/Activity for our image. To get further help one could use the Loaders class, which helps in surviving the Activity switch.

In addition to the final callback of the loading operation, we can also get notified periodically during the loading process by overriding the onProgressUpdate() method. The callback has to be triggered in the doInBackground() method by invoking the AsyncTasks method publishProgress().

One feature omitted from the AsyncTask class is the ability deal with potential errors in the doInBackground() method. It is therefore advisable to catch any exceptions and to store them in a field so that we can optionally display a warning/error when propagating the result to the UI.

An interesting variation of the mechanism described above can be found on the Android developers blog.

Over time there have been several changes to the way Android deals with AsyncTasks that run concurrently. In very old Android versions (pre-1.6 afaik) multiple AsyncTasks were executed in sequence. That behavior has been changed to run the AsyncTasks in parallel up until Android 2.3. Beginning with Android 3.0 the the Android team decided that people were not careful enough with synchronizing the tasks that run in parallel and switched the default behavior back to sequential execution. Internally the AsyncTask uses an ExecutionService that can be configured to run in sequence (default) or in parallel as required:

ImageLoader imageLoader = new ImageLoader( imageView );
imageLoader.executeOnExecutor( AsyncTask.THREAD_POOL_EXECUTOR, "https://url.com/image.png" );

Loading an image via HTTP

So far we have looked at the Android mechanism to deal with concurrency and the Android Activity life cycle. It is time to get down into the network and load the image data itself. Android ships with Java’s standard java.net.* HTTP classes and the Apache HttpClient built in.

Which HTTP implementation to choose is a topic of ongoing debate but lately the Android team favors the URLConnection over the Apache HttpClient. The Apache HttpClient was faster and more stable in earlier versions of Android. Starting with Android 2.3, using the URLConnection is recommended, as it has a more compact API, offers the same feature set and gets better support from the Android team. With the release of Android 4.0 (Ice Cream Sandwich) the URLConnection also supports content caching (more on that later). Note that older versions of the Apache HttpClient contain a bug when directly converting the data InputStream into a Bitmap. The solution is to the stream in another stream as described here. For the most robust loading mechanism you should cache the incoming data in memory/disc. A simple/naive implementation to load an image via the URLConnection could look like this:

URLConnection conn = new URL( url ).openConnection();
conn.connect();
return BitmapFactory.decodeStream( conn.getInputStream() );

Caching images

Often an image is displayed several times. A classic scenario is a thumbnail that can be clicked and in response, a bigger version of the image pops-up. To reduce bandwidth drain when displaying an image multiple times, we can apply a caching mechanism.

In the example above we streamed the image directly into the BitmapFactory to create a Bitmap image. When preserving the image we can either cache the raw data before it is deflated into a Bitmap object or we take the Bitmap and store it in the cache. The first version saves device RAM whereas the second reduces computation time. Naturally, the trade-off should be evaluated based on the use case.

[ Want to develop mobile apps faster? Try Tabris. Download the 30-day trial. ]

In both cases we can use an LRUCache to store the images. The Android Javadocs describe the LRUCache as, “A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.” This means that we will have to ask the cache for our image and if it is not found, we load it and place it into the cache.

The LRUCache is an in-memory cache. This means that there is a finite number of images we can cache. A good approach to determine the cache size is to calculate it based on the available heap memory:

int memClass = ( ( ActivityManager )activity.getSystemService( Context.ACTIVITY_SERVICE ) ).getMemoryClass();
int cacheSize = 1024 * 1024 * memClass / 8;
LruCache cache = new LruCache( cacheSize );

We use a Bitmap method to determine the size of each element put into the cache. In this example we use 1/8 of the available heap memory. The cache size should be increased if required.

public class ImageCache extends LruCache {

  public ImageCache( int maxSize ) {
    super( maxSize );
  }

  @Override
  protected int sizeOf( String key, Bitmap value ) {
    return value.getByteCount();
  }

  @Override
  protected void entryRemoved( boolean evicted, String key, Bitmap oldValue, Bitmap newValue ) {
    oldValue.recycle();
  }
  
}

Since the LRUCache is constrained by the device RAM we can work around that limitation by creating a two-stage cache that uses the second disc based LRUCache if the data cannot be found in the RAM based LRUCache. Android already offers such an implementation in the form of the DiskLruCache. The disk-based cache should hold onto the raw image files and would need to convert them to a Bitmap when accessed.

Wrap-Up

In Part 1 of this two-part series we have looked at image loading and caching. In the next installment we will dive deeper into caching mechanisms and look at other implementations for our cache. We will also have a closer look at how to display an image to provide the best user experience. Finally, we will compare several open source libraries that can help you with the challenges described here.

Add me on Google+