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 (viaFragment.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.
No comments:
Post a Comment