One challenge that developers commonly face is displaying images from a network location. This challenge often comes in different forms, such as displaying many images in a list. An ideal solution for this type of challenge will include
Maintaining a responsive UI
Performing network and disk I/O outside the application’s UI thread Support for view recycling, as in the case of a ListView
A caching mechanism for quickly displaying images
Many solutions to this problem use an in-memory cache for holding previously loaded images and a thread pool for queuing up images to load. But an often-overlooked fea- ture is the order in which images are requested.
Consider the case of a ListView where each row contains an image. If a user “flings” the list in the downward direction, most image-loading solutions will request each image in the order its parent View is displayed on the screen. As a result, when the user stops scrolling, the rows currently on the screen, which are the most impor- tant rows at the current point in time, will load last. What you want is for the last- requested rows to “jump the queue” and be processed first.
40.1
Starting point: Android sample application
The Android Training section of the official documentation includes the article (see section 40.6) “Displaying Bitmaps Efficiently,” which we’ll use as our starting point. The article covers core concepts such as downsampling images to the proper size, using the LruCache class for in-memory caching (available in the Support Library, ver- sion 4), and a basic mechanism for performing work off the UI thread.
We’ll expand on this example application to meet the goal of loading the most recently requested images first. We’ll also make performance improvements over the
129
Last-in-first-out image loading
original version by removing the problematic use of one AsyncTask instance per get- View() call by the application’s adapter. The sample implementation makes it possi- ble to cause a runtime exception when scrolling up and down several times, resulting in a RejectedExecutionException caused by too many AsyncTask instances, so that’s fixed in the final example.
40.2
Introducing executors
The AsyncTask solution isn’t suitable for large number of images, nor will it give us control over the priority of our tasks. Instead, we’ll use an executor service from the java.util.concurrent package and a priority queue to specify the order in which we request images. With the new implementation, we can maintain methods similar to AsyncTask, namely, cancelling tasks which have been pushed offscreen. Our last-in-first-out (LIFO) implementation will involve two classes, LIFOTask and LIFO- ThreadPoolProcessor.
Our new task object will maintain a static variable indicating the number of instances created. This will serve as the priority for the task, because a newly created task will have a higher counter. We use this counter to implement a compareTo() method, for sorting purposes later:
public class LIFOTask extends FutureTask<Object> implements Comparable<LIFOTask> {
private static long counter = 0; private final long priority;
public LIFOTask(Runnable runnable) { super(runnable, new Object()); priority = counter++;
Tasks in this example are all created on the same thread.
}
public long getPriority() { return priority;
}
@Override
public int compareTo(LIFOTask other) {
return priority > other.getPriority() ? -1 : 1; }
}
Our choice of base class here is important. We extend FutureTask, a class accepted by the executor classes because it exposes a cancel method, much like the old implemen- tation using AsyncTask.
Building off the LIFOTask class, we’ll use its compareTo() method and the Thread- PoolExecutor class:
public class LIFOThreadPoolProcessor { private BlockingQueue<Runnable> opsToRun =
new PriorityBlockingQueue<Runnable>(64, new Comparator<Runnable>() { @Override
if (r0 instanceof LIFOTask && r1 instanceof LIFOTask) { LIFOTask l0 = (LIFOTask)r0; LIFOTask l1 = (LIFOTask)r1; return l0.compareTo(l1); } return 0; } });
private ThreadPoolExecutor executor;
public LIFOThreadPoolProcessor(int threadCount) {
executor = new ThreadPoolExecutor(threadCount, threadCount, 0, TimeUnit.SECONDS, opsToRun);
}
public Future<?> submitTask(LIFOTask task) { return executor.submit(task);
}
public void clear() { executor.purge(); }
}
The noteworthy part of the class is the parameters passed to the ThreadPoolExecutor constructor. We let the client application choose the exact thread pool size, and choose a PriorityBlockingQueue to hold the incoming tasks that the client applica- tion submits. We then use the compareTo() method of the LIFOTask object to get our desired ordering. Note that in this case, the keepAlive parameter is not applicable given the core and max thread pool sizes used.
40.3
UI thread—leaving and returning seamlessly
As Android developers, we know the importance of maintaining a responsive UI, so we offload time-consuming tasks, like I/O, to a background thread. Often, when this work is done, we want to update the UI. Android, much like other UI systems you may be familiar with, isn’t thread-safe. We must return to the main application thread before modifying any ImageViews. Attempting to modify the UI from outside the main thread will cause an exception.
The original implementation used the onPostExecute() method of AsyncTask. Because we’re replacing the use of AsyncTask with an executor, we’ll instead give a Runnable to our host activity. We’ll use the runOnUiThread() method of the Activity class, which will use a Handler under the hood to get our work added to the UI’s mes- sage queue.
Slipping something into the UI thread doesn’t come free of consideration. We have to be mindful of the following:
ImageView instances may be recycled if a user scrolls in a ListView. The host activity may be destroyed before a task finishes.
131
Last-in-first-out image loading
As a result, every step of the Runnable used to process images checks if it should stop performing work. A stop condition is detected if the host activity sets a flag with ImageWorker’s setExitTasksEarly() method, which should be called from onPause(). Additionally, a stop condition is detected if the cancel() method of FutureTask is called.
40.4
Considerations
For use in a production application, the Android Training article suggests using a bet- ter disk-caching solution. The implementation provided in the original article is lack- ing in a few key areas. To provide a more complete example here, the disk cache implementation was modified to support rebuilding the disk cache upon application restarts, and no longer maintains two copies of downloaded files.
40.5
The bottom line
Time-consuming work, such as loading images, needs to be performed outside the UI
thread. This will allow built-in components, such as ListView, to operate smoothly. You can give users a better experience by fine-tuning the order in which you load images using a LIFO queue.
Using a potentially unbounded number of AsyncTask instances is problematic, and the job can be better fulfilled by using executors. Additionally, Android provides a solid implementation of LruCache in the support library for implementing efficient caching solutions.
40.6
External links
http://developer.android.com/training/displaying-bitmaps/index.html http://developer.android.com/tools/extras/support-library.html#Using http://developer.android.com/reference/java/util/concurrent/ExecutorService.html http://developer.android.com/reference/java/util/concurrent/FutureTask.html133