Sunday, April 28, 2013

The dark side of AsyncTask

Hi everyone,

In this post I'm going to talk about some of the (less known) problems of AsyncTask. I've written my first post on how to use AsyncTask about half a year ago. Now I'm going to tell you what problems they might cause, how you can fix this and what other options you have.

AsyncTask (°2009 - ...)

AsyncTask was added to Android in API level 3 (Android 1.5 Cupcake) and was said to help developers manage their threads. It's actually also available for 1.0 and 1.1, under the name of UserTask. It has the same API as AsyncTask. You should just copy the source code in your application to use it. But according to the Android Dashboards, there are less 0.1% of all Android devices that run on these API levels, so I mostly don't bother to support them.

Right now, AsyncTask is probably the most commonly used technique on Android for background execution. It's really easy to work with and that's what developers love about it. But this class has a couple of downsides and we are not always aware of them.

Lifecycle

There is quite a misunderstanding about our AsyncTask. Developers might think that when the Activity that created the AsyncTask has been destroyed, the AsyncTask will be destroyed as well. This is not the case. The AsyncTask keeps on running, doing his doInBackground() method until it is done. Once this is done it will either go to the onCancelled(Result result) method if cancel(boolean) is invoked or the onPostExecute(Result result) method if the task has not been cancelled.

Suppose our AsyncTask was not cancelled before our Activity was destroyed. This could make our AsyncTask crash, because the view it wants to do something with, does not exist anymore. So we always have to make sure we cancel our task before our Activity is destroyed.  The cancel(boolean) method needs one parameter: a boolean called mayInterruptIfRunning. This should be true if the thread executing this task should be interrupted; otherwise, in-progress tasks are allowed to complete. If there is a loop in our doInBackground() method, we might check the boolean isCancelled() to stop further execution.

So we have to make sure the AsyncTask is always cancelled properly. 

Does cancel() really work?

Short answer: Sometimes.
If you call cancel(false), it will just keep running until its work is done, but it will prevent onPostExecute() to be called. So basically, we let our app do pointless work. So let's just always call cancel(true) and the problem is fixed, right? Wrong. If mayInterruptIfRunning is true, it will try to finish our task early, but if our method is uninterruptable such as BitmapFactory.decodeStream(), it will still keep doing the work. You could close the stream prematurely and catch the Exception it throws, but this makes cancel() a pointless method.

Memory leaks

Because an AsyncTask has methods that run on the worker thread (doInBackground()) as well as methods that run on the UI (e.g. onPostExecute()), it has took keep a reference to it's Activity as long as it's running. But if the Activity has already been destroyed, it will still keep this reference in memory. This is completely useless because the task has been cancelled anyway.

Losing your results

Another problem is that we lose our results of the AsyncTask if our Activity has been recreated. For example when an orientation change occurs. The Activity will be destroyed and recreated, but our AsyncTask will now have an invalid reference to its Activity, so onPostExecute() will have no effect. There is a solution for this. You can hold onto a reference to AsyncTask that lasts between configuration changes (for example using a global holder in the Application object). Activity.onRetainNonConfigurationInstance() and Fragment.setRetainedInstance(true) may also help you in this case.

Serial or parallel?

There is a lot of confusion about AsyncTasks running serial or parallel. This is normal because it has been changed a couple of times. You could probably be wondering what I mean with 'running serial or parallel'. Suppose you have these two lines of code somewhere in a method:

new AsyncTask1().execute();
new AsyncTask2().execute();

Will these two tasks run at the same time or will AsyncTask2 start when AsyncTask1 is finished?
Short answer: It depends on the API level.

Before API 1.6 (Donut):

In the first version of AsyncTask, the tasks were executed serially. This means a task won't start before a previous task is finished. This caused quite some performance problems. One task had to wait on another one to finish. This could not be a good approach.

API 1.6 to API 2.3 (Gingerbread):

The Android developers team decided to change this so that AsyncTasks could run parallel on a separate worker thread. There was one problem. Many developers relied on the sequential behaviour and suddenly they were having a lot of concurrency issues.

API 3.0 (Honeycomb) until now

"Hmmm, developers don't seem to get it? Let's just switch it back." The AsyncTasks where executed serially again. Of course the Android team provided the possibility to let them run parallel. This is done by the method executeOnExecutor(Executor). Check out the API documentation for more information on this.

If we want to make sure we have control over the execution, whether it will run serially or parallel, we can check at runtime with this code to make sure it runs parallel:

 
 public static void execute(AsyncTask as) {
  if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR1) {
   as.execute();
  } else {
   as.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  }
 }
(This code does not work for API lvl 1 to 3)

Do we need AsyncTasks?

Not really. It is an easy way to implement background features in our app without having to write a lot of code, but as you can see, if we want it to work properly we have to keep a lot of things in mind. Another way of executing stuff in background is by using Loaders. They were introduced in Android 3.0 (Honeycomb) and are also available in the support library. Check out the developer guide for more information.