The Hidden Pitfalls of AsyncTask

Logc.at is a defunct blog I wrote a couple posts for a few years ago. I'm using the Wayback Machine to recover and repost my articles here.

I originally wrote this article when I was (foolishly) still using AsyncTasks. Nowadays I simply consider it a mistake in all cases. As you'll see from the original article, there are a lot of problems with it - and there are much better solutions.

My preferred alternative these days are combining RxJava with schedulers. You get the same effect as an AsyncTask with none of the problems, plus you get an awesome framework in addition. I know, recommending a library to solve a problem is irritating, but RxJava is worth looking at for many reasons.


Originally posted November 8, 2011

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 through Activity.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 via AsyncTask.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 call AsyncTask.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.

comments powered by Disqus