When AsyncTask was introduced to Android, it was labeled as “Painless Threading.” Its goal was to make background Threads which could interact with the UI thread easier. It was successful on that count, but it’s not exactly painless – there are a number of cases where AsyncTask is not a silver bullet. It is easy to blindly use AsyncTask without realizing what can go wrong if not handled with care. Below are some of the problems that can arise when using AsyncTask without fully understanding it.
AsyncTask and Rotation
AsyncTask’s primary goal is to make it easy to run a Thread in the background that can later interact with the UI thread. Therefore the most common use case is to have an AsyncTask run a time-consuming operation that updates a portion of the UI when it’s completed (in AsyncTask.onPostExecute()).
This works great… until you rotate the screen. When an app is rotated, the entire Activity is destroyed and recreated. When the Activity is restarted, your AsyncTask’s reference to the Activity is invalid, so onPostExecute() will have no effect on the new Activity. This can be confusing if you are implicitly referencing the current Activity by having AsyncTask as an inner class of the Activity.
The usual solution to this problem is to hold onto a reference to AsyncTask that lasts between configuration changes, which updates the target Activity as it restarts. There are a variety of ways to do this, though they either boil down to using a global holder (such as in the Application object) or passing it throughActivity.onRetainNonConfigurationInstance(). For a Fragment-based system, you could use a retained Fragment (via Fragment.setRetainedInstance(true)) to store running AsyncTasks.
AsyncTasks and the Lifecycle
Along the same lines as above, it is a misconception to think that just because the Activity that originally spawned the AsyncTask is dead, the AsyncTask is as well. It will continue running on its merry way even if you exit the entire application. The only way that an AsyncTask finishes early is if it is canceled viaAsyncTask.cancel().
This means that you have to manage the cancellation of AsyncTasks yourself; otherwise you run the risk of bogging down your app with unnecessary background tasks, or of leaking memory. When you know you will no longer need an AsyncTask, be sure to cancel it so that it doesn’t cause any headaches later in the execution of your app.
Cancelling AsyncTasks
Suppose you’ve got a search query that runs in an AsyncTask. The user may be able to change the search parameters while the AsyncTask is running, so you callAsyncTask.cancel() and then fire up a new AsyncTask for the next query. This seems to work… until you check the logs and realize that your AsyncTasks all ran till completion, regardless of whether you called cancel() or not! This even happens if you pass mayInterruptIfRunning as true – what’s going on?
The problem is that there’s a misconception about what AsyncTask.cancel() actually does. It does not kill the Thread with no regard for the consequences! All it does is set the AsyncTask to a “cancelled” state. It’s up to you to check whether the AsyncTask has been canceled so that you can halt your operation. As for mayInterruptIfRunning – all it does is send an interrupt() to the running Thread. In the case that your Thread is uninterruptible, then it won’t stop the Thread at all.
There are two simple solutions that cover most situations: Either check AsyncTask.isCancelled() on a regular basis during your long-running operation, or keep your Thread interruptible. Either way, when you call AsyncTask.cancel() these methods should prevent your operation from running longer than necessary.
This advice doesn’t always work, though – what if you’re calling a long-running method that is uninterruptible (such as BitmapFactory.decodeStream())? The only success I’ve had in this situation is to create a situation which causes an Exception to be thrown (in this case, prematurely closing the stream that BitmapFactory was using). This meant that cancel() alone wouldn’t solve the problem – outside intervention was required.
Limitations on Concurrent AsyncTasks
I’m not encouraging people to start hundreds of threads in the background; however, it is worth noting that there are some limitations on the number of AsyncTasks that you can start. The modern AsyncTask is limited to 128 concurrent tasks, with an additional queue of 10 tasks (if supporting Android 1.5, it’s a limit of ten tasks at a time, with a maximum queue of 10 tasks). That means that if you queue up more than 138 tasks before they can complete, your app will crash. Most often I see this problem when people use AsyncTasks to load Bitmaps from the net.
If you are finding yourself running up against these limits, you should start by rethinking your design that calls for so many background threads. Alternatively, you could setup a more intelligent queue for your tasks so that you’re not starting them all at once. If you’re desperate, you can grab a copy of AsyncTask and adjust the pool sizes in the code itself.
Categories: Android | Permalink
November 9, 2011 at 1:40 pm
> AsyncTask and Rotation
I used the same way. Then I spoke to another android developer. He suggested anotehr easier solution – to add android:configChanges=”orientation|keyboard|keyboardHidden”
It works fine if you use the same layout for your activity in landscape and protrait mode.
November 9, 2011 at 2:26 pm
Yeah Mur Votema, I use android:configChanges=”orientation|keyboard|keyboardHidden” with no problem.
November 9, 2011 at 4:01 pm
I’m not a big fan of overriding configuration changes in order to solve the rotation issue. Like you said, it only works if you use the exact same layout between landscape/portrait, which I think is a fairly large assumption to make. I grant that there are some layouts that can be the same between the two, but good design usually necessitates at least a little tweaking between different screen orientations.
November 10, 2011 at 9:11 am
Glad that someone finally has a explanation about AsyncTask pitfalls! I had a hard time debugging AsyncTask problem but haven’t had time to write it down.
I’ve also noticed the 138 tasks limit problem, but strangely it’s not “10 concurrent tasks with a queue that has 128 pending tasks max”, but it seems that 138 is “128 concurrent tasks with a queue that has 10 pending tasks max”
It does seem weird, but from the 2.3 source:
private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;
private static final BlockingQueue sWorkQueue =
new LinkedBlockingQueue(10);
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, “AsyncTask #” + mCount.getAndIncrement());
}
};
private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);
If I understand correctly, if you give 138 tasks in one time, it will spawn 128 threads and then put the remaining 10 tasks in the queue. It’s so weird.
November 10, 2011 at 8:57 pm
Ah yes, you’re correct. I was misreading the code – fixed it in the article.
That said, one should try to avoid spawning 128 threads at once.
January 2, 2012 at 2:54 am
Thanks for writing this, I hadn’t been totally clear on how AsyncTask handles some of these situations when left to its own devices, and it had been bothering me.
How do you know that BitmapFactory.decodeStream() is uninterruptible? I don’t see anything in the Android source spelling it out explicitly. Is it because it’s a native method? Are are all native methods uninterruptible?
January 2, 2012 at 3:25 pm
Just from personal experience (specifically, running into the thread limit due to canceling image tasks without realizing they weren’t being finished due to the uninterruptible nature of that method). To be honest I’ve never investigated why exactly that method is uninterruptible.