AsyncTask comes up in conversation quite often around the office, usually because of a failed code review or some related bug that’s been discovered. It’s one of the most well understood classes on the Android platform, and yet it’s also one of the hardest classes to use correctly. Even advanced Android developers get it wrong.
In the following discussion, we’ll take a look at a few snippets of code, touch on some of their problems, and arrive (hopefully) at a pattern of AsyncTask usage that’s suitable for most cases.
Let’s start with a fairly typical example.
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Find views and assign them to member variables. new AsyncTask<void, void,="" string="">() { @Override protected String doInBackground(Void... params) { // Do some long running task. return result; } @Override protected void onPostExecute(String result) { // Update UI } }.execute(); } // Various View member variables. } </void,> |
This seems fairly straightforward. When our Activity is created, we launch an AsyncTask to perform some background work, which is performed in doInBackground. When the background work is finished, doInBackground returns a String which is used by onPostExecute to safely update the UI.
We’re using the AsyncTask here exactly as I’ve seen it used in production code. UI elements are being updated safely. Work is being done in the background. We’re in no danger of an ANR message. So what exactly is the problem here?
The problem isn’t what happens inside the AsyncTask. The problem is what happens outside of it. Specifically, what happens if the Activity is destroyed while the AsyncTask is running?
Imagine this scenario – the Activity is created, the AsyncTask is instantiated and executed, and then the screen orientation changes. When screen orientation changes under normal circumstances, the original Activity is destroyed and a new one is created. This means another AsyncTask will be instantiated and the background work will start all over again.
But that’s not the worst of it. The original AsyncTask will continue to run until it’s finished. And once it’s finished, it’ll update UI elements that aren’t visible anymore. And because the original AsyncTask still has an implicit outerclass pointer to the original Activity, the original Activity can’t be garbage collected until the original AsyncTask is finished. Change the screen orientation a bunch of times on a resource intensive Activity and you risk running out of memory.
There’s also a limit on the number of AsyncTasks you can have running at once. For Activities with smaller footprints, you’ll most likely run out of AsyncTasks before you start to see memory problems appear.
Does that make you uncomfortable? It certainly should.
So how do we work around these issues? Let’s address these one at a time, starting with the simplest – the implicit pointer to the outer class. This can be solved by making the AsyncTask class static, which also means it can no longer be anonymous.
Removing Implicit Outerclass Pointers
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Find views and assign them to member variables. MyTask task = new MyTask(); task.execute(); } static class MyTask extends AsyncTask<void, void,="" string=""> { @Override protected String doInBackground(Void... params) { // Do some long running task. return result; } @Override protected void onPostExecute(String result) { // Need to update UI. But how? } } // Various View member variables. } </void,> |
So far so good. Our AsyncTask will no longer keep a reference to the Activity. But now we can’t update the UI when the task completes, which kind of defeats the purpose. How do we fix this? By giving back an Activity reference to the AsyncTask. “Wait, isn’t that what we tried to eliminate by making the class static?” Well, yes. But we wanted to remove the implicit reference, which is something we couldn’t change at runtime. With something a bit more explicit, we have direct control over when the reference comes and goes.
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Find views and assign them to member variables. m_task = new MyTask(); m_task.m_activity = this; m_task.execute(); } @Override public void onDestroy() { super.onDestroy(); m_task.m_activity = null; } static class MyTask extends AsyncTask<void, void,="" string=""> { @Override protected String doInBackground(Void... params) { // Do some long running task. return result; } @Override protected void onPostExecute(String result) { if (m_activity != null) { // Update UI } } MyActivity m_activity = null; } private MyTask m_task = null; // Various View member variables. } </void,> |
That’s better. In the above example we remove the Activity reference in the AsyncTask when the Activity is destroyed. The AsyncTask can continue to run without preventing our Activity from being garbage collected.
Concurrent AsyncTask Limitations
Now let’s briefly discuss the problem of concurrent AsyncTasks. If you weren’t aware that there’s an upper limit to concurrent AsyncTasks, Google “AsyncTask limits” and you’ll find plenty of information about it. We need to control the lifetime of our AsyncTask instance. What that boils down to is canceling the task and letting the garbage collector deal with it. Canceling a task is a two-step process. First, we set the AsyncTask’s cancelled flag in the Activity’s onDestroy method.
m_task.cancel(false); |
This alone doesn’t actually cancel the task. It has no effect on the background process if the task chooses to ignore it. So we also need to regularly check the return value of isCancelled() inside of our AsyncTask’s doInBackground method and bail out appropriately. This allows us to terminate that background process cleanly.
if (isCancelled()) { return null; } |
Notice that we passed false to the AsyncTask’s cancel method. If you pass true, it interrupts the thread and can leave things in a particular awkward state. See the Thread class’s interrupt method for details.
One AsyncTask To Rule Them All
We still have a problem of a new AsyncTask being created and executed every time our Activity is destroyed and recreated. In most cases, creating a new AsyncTask instance is unnecessary. We often only need the AsyncTask to query a database, make a web service request, or read some file once. So it’d be nice if we could leverage a single AsyncTask no matter how many times our Activity is temporarily destroyed/created.
I’ve seen some folks set the android:configChanges attribute in the Activity’s XML declaration inside the app manifest just to avoid the redundant AsyncTask issue. And while it’s certainly doable, it’s somewhat of an obscure solution. android:configChanges is meant to solve other problems. That approach may confuse other developers who will need to maintain your code. It also makes the UI a bit inflexible because the default behavior may actually be behavior you want.
I’ve also seen some folks address the issue using invisible Fragments. Again, it works. But it’s somewhat obscure if you’re using a Fragment oriented approach just to solve this issue. Make sure your Fragment usage is appropriate for your UI design. Never use a sledgehammer when a rubber mallet will do.
The approach I like best is mentioned in this StackOverflow thread and in HervĂ© Guihot’s book “Pro Android Apps Performance Optimization”.
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Find views and assign them to member variables. m_task = (MyTask) getLastNonConfigurationInstance(); if (m_task != null) { m_task.m_activity = this; } else { m_task = new MyTask(); m_task.m_activity = this; m_task.execute(); } } @Override public void onDestroy() { super.onDestroy(); m_task.m_activity = null; if (this.isFinishing()) m_task.cancel(false); } @Override public Object onRetainNonConfigurationInstance() { return m_task; } static class MyTask extends AsyncTask<void, void,="" string=""> { @Override protected String doInBackground(Void... params) { // Do some long running task. We need to make sure // we peridically check the return value of isCancelled(). return result; } @Override protected void onPostExecute(String result) { if (m_activity != null) { // Update UI } } MyActivity m_activity = null; } private MyTask m_task = null; // Various View member variables. } </void,> |
The key piece here is the use of the onRetainNonConfigurationInstance and getLastNonConfigurationInstance methods to reuse an AsyncTask instance between build up and tear down of the Activity. Using this approach will always result in a single AsyncTask being created and used.
My code above differs a bit from that mentioned in the StackOverflow discussion and in Guihot’s book. One notable difference is that I check to see if the Activity is actually finishing in the onDestroy method. If it is, we cancel the task. isFinishing will return true if this Activity is being dismissed as a result of going backwards in the Activity stack.
Our Final Approach
There is still a bug here. Can you spot it? What happens if the Activity has been destroyed and recreated after the AsyncTask has completed? Our UI doesn’t get updated. The solution to this is fairly simple and illustrated in the final version of our Activity below.
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Find views and assign them to member variables. m_task = (MyTask) getLastNonConfigurationInstance(); if (m_task != null) { m_task.m_activity = this; if (m_task.m_isFinished) m_task.updateUI(); } else { m_task = new MyTask(); m_task.m_activity = this; m_task.execute(); } } @Override public void onDestroy() { super.onDestroy(); m_task.m_activity = null; if (this.isFinishing()) m_task.cancel(false); } @Override public Object onRetainNonConfigurationInstance() { return m_task; } static class MyTask extends AsyncTask<void, void,="" string=""> { @Override protected String doInBackground(Void... params) { // Do some long running task. We need to make sure // we peridically check the return value of isCancelled(). return result; } @Override protected void onPostExecute(String result) { m_result = result; m_isFinished = true; updateUI(); } public void updateUI() { if (m_activity != null) { // Update UI using m_result } } // These should never be accessed from within doInBackground() MyActivity m_activity = null; boolean m_isFinished = false; String m_result = null; } private MyTask m_task = null; // Various View member variables. } </void,> |
I don’t claim that this pattern is a one-sized fits all solution. But I think it covers most cases quite well. There are a few things I avoided discussing here, such as dealing with progress dialogs and how to design tasks that can be used by multiple activities. I didn’t want to add too much complexity to the examples. And I think those sorts of things should be fairly intuitive once you’ve achieved a certain degree of comfort with AsyncTask