Android Optimizations for Responsiveness: With a Warning About Leaking Activities
This article was written by Jay Slater, one of our software engineers.
In the interest of broadening my horizons and being a better-rounded developer, I spend some of my time off of work on computer science pursuits. One of my recent projects has to do with board games and artificial intelligences, a field of endeavor where pure speed is king, programmers spend their time chasing incremental improvements to the average case, and tricky micro-optimization is the order of the day.
I put the question to myself: does that sort of thinking have a place in Android development? In a word, the answer is no. Outside of a few particular fields, to focus on pure speed as a performance metric is to miss the forest for the trees. A developer can get away with all sorts of long-running tasks, provided that they do not interrupt the user’s experience, and that they provide feedback on their progress.
This brings me to my first point: design for responsiveness. Do you have a big task to do? Do it in the background, and provide a notification for the user to track progress. Must your task be completed before your app can be used? Make it clear to your user with a progress bar. These are simple lessons, and to their credit, most app developers get them right, but there are a few caveats as regard Android’s mechanisms for running tasks in the background. Ask an Android developer how to put a task on a background thread, and most of the time, you will get the answer, “Use an AsyncTask.” Most of the time, this is the correct answer: AsyncTasks are handy tools for carrying out work asynchronously, and in most Android apps, they will end up being used heavily.
Beware, though: if you have a large app which uses AsyncTasks heavily, or an app of any size which uses AsyncTasks for especially long-running tasks which may overlap, you will run into trouble. On modern versions of Android, AsyncTasks are guaranteed to be executed in order, which is another way of saying that, by default, all AsyncTasks are executed on the same background thread. If you have an AsyncTask for a big network upload, say, and an AsyncTask to handle some UI action, and the network upload happens first. Suddenly, your UI becomes unresponsive in the worst way: the app has not frozen, but manipulating the UI does not immediately have the expected result.
Android acknowledges that you may want to run long-running background tasks as well as short blocking tasks at the same time, though, and provides you two options. First, you can use AsyncTask’s executeOnExecutor method, passing in THREAD_POOL_EXECUTOR. The thread pool executor runs tasks in true parallel, and requires very little rewriting.
That said, though the Android documentation pushes AsyncTasks for almost all background tasks, in truth, AsyncTasks are best for relatively fast tasks which should nevertheless happen in the background: small amounts of file I/O, for instance, or short network operations. For truly long-running operations like file uploads or downloads, Android provides a better mechanism: the IntentService. IntentService is a concrete implementation of Service, which handles almost everything for you: Service lifecycle, spawning a separate worker thread, queueing requests to the IntentService, and handling interruptions and thread lifecycle. All you have to do is write code to do your task. Though requests to a specific IntentService will queue, requests to one IntentService will not block requests to another IntentService, and will not interrupt application logic.
Although the best way to design a responsive app is to do as little work as possible in the app itself, sometimes you have no choice: if the work must be done, and there is nowhere to offload it to, it has to happen. Fortunately, Android provides you with plenty of functionality to keep your app responsive while doing it. Unfortunately, there are constraints on your app which cannot be so easily set aside: memory and I/O resources.Your app gets a relatively small pool of memory out of a relatively small amount of memory to begin with, relative to desktop applications. You should use it carefully. Be sure to close file readers and network connections when you are done with them: Android pulls from a fixed-size pool. These and other lessons are simply Java best practice, and such lessons apply, in most cases, to Android. Since many articles have been written on Java best practice, I will focus instead on a particular Android anti-pattern, one which can have dire consequences: leaking Activities. Here are two examples.
Consider an application with a large bitmap resource which you do not want to reload. You make a static Drawable field to hold it. You load your bitmap resource into the field the first time the application loads. You have leaked an Activity reference: Drawables hold a reference their host Views, and Views hold a reference to their parent Activity. As long as the reference to the Drawable is maintained, a reference to a potentially dead Activity is also maintained.
Consider a private inner class which extends Handler, belonging to an Activity which wants to process messages. A message is posted to the Handler, waiting for a certain background task to finish. The Activity closes. You have leaked an Activity reference again: non-static private inner classes hold an implicit reference to their outer class, and the reference will only go away when the Handler receives the message.
Leaking an Activity is very bad: Activities hold references to their entire view hierarchies, to include any loaded resources, and depending on your code, possibly a whole lot more. Leaking a dozen Activities could represent a significant chunk of the heap space available to your application.
How can you avoid leaking Activities? In the two example cases given, the answer is obvious: in the first, avoid keeping static references to Views, Drawables, or other UI components. In the second, make your inner classes static, unless they are never exposed to the outside world. More broadly, adhere to the following principle: use a Context with the same lifecycle as your object. A helper object in an Activity, provided that it is not referenced outside that Activity, can safely use the Activity as its Context. An object which will remain alive as long as the app is alive should use the application Context (available from any Context object by the getApplicationContext() method). Alternately, if your application design requires access to a Context from an object which does not share its lifecycle, consider holding a WeakReference rather than a standard reference. WeakReferences are not strong enough to hold an object in memory, so a Context which is only weakly-referenced from objects which will outlive it will not remain in memory.
Finally, remember the first lesson of optimization: never optimize prematurely! The overwhelming majority of Android applications do not handle enough information to require special optimization beyond simple good design, and if your application does not fit into the majority, you probably know already. If your app belongs to that category, or if the tips herein have not been sufficient, stay tuned: in the coming months, I hope to write again on more traditional Java optimizations, with an eye specifically toward how they affect development on Android platforms.