Random An Android blog

17Apr/144

AsyncTask is bad and you should feel bad

There are a lot of mixed feelings about AsyncTask. All this stems from the fact that developers new to Android jump to AsyncTask when they need to do something on a thread, without fully understanding the Android lifecycle and how to properly handle it. This often leads to problems with leaked activities, state not updating, and a variety of other issues.
Because of this you'll likely get told from experienced developers that you should not be using AsyncTask.

In this article I'll give an example of how you can use AsyncTask and avoid these issues.

I know that the general consensus is that AsyncTask isn't meant for long-running tasks, but sometimes you just want a simple way to perform a simple task (no matter how long it might take). In my opinion, AsyncTask is perfect for this.

You're doing it wrong
A common way for new developers to use AsyncTask could be something like the following:

public class MainActivity extends Activity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

  // Somewhere the AsyncTask is started

  public class MyAsyncTask extends AsyncTask<Void, Void, String> {

    @Override protected String doInBackground(Void... params) {
      // Do work
      return result;
    }

    @Override protected void onPostExecute(String result) {
      Log.d("MyAsyncTask", "Received result: " + result);
    }
  }
}

The problem with this is that the AsyncTask has an implicit reference to the enclosing Activity. If a configuration change happens the Activity instance that started the AsyncTask would be destroyed, but not GCd until the AsyncTask finishes. Since Activities are heavy this could lead to memory issues if several AsyncTasks are started. Another issue is that the result of the AsyncTask could be lost, if it's intended to act on the state of the Activity.

This leads to two issues we have to fix:
- Ensuring the Activity isn't kept in memory when destroyed by the framework
- Ensuring the result of the AsyncTask is delivered to the current Activity instance

There's some examples out there that uses Activity#onRetainNonConfigurationInstance (https://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance()) to pass the AsyncTask instance between Activities on configuration changes, and then set a callback field on the AsyncTask with the current Activity instance. I'm not particularly fond of that approach since then you have to keep track of your AsyncTasks to make sure they're all passed properly to the new instance. In a Fragment you'd use Fragment#setRetainInstance (https://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean)) to achieve the same, but you don't necessarily want to do that.

Otto to the rescue
My favorite approach is to use an event bus - I prefer Otto (http://square.github.io/otto/).

Basically what we're going to do is
- Move the AsyncTask out into its own class file
- Define an event that's posted once the AsyncTask completes
- Subscribe to said event in our Activity

Using Otto is quite simple. We have to use it as a singleton, so create a small helper class for this

public class MyBus {

  private static final Bus BUS = new Bus();

  public static Bus getInstance() {
    return BUS;
  }
}

For this example the result will just be a String, so the event we post is as simple as this

public class AsyncTaskResultEvent {

  private String result;

  public AsyncTaskResultEvent(String result) {
    this.result = result;
  }

  public String getResult() {
    return result;
  }
}

Then we have to create an AsyncTask that does some work, then posts the result to the event bus. In this case it'll sleep for a random amount of time.

public class MyAsyncTask extends AsyncTask<Void, Void, String> {

  @Override protected String doInBackground(Void... params) {
    Random random = new Random();
    final long sleep = random.nextInt(10);
    try {
      Thread.sleep(sleep * 1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return "Slept for " + sleep + " seconds";
  }

  @Override protected void onPostExecute(String result) {
    MyBus.getInstance().post(new AsyncTaskResultEvent(result));
  }
}

Our activity layout will just be a button the starts an AsyncTask

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

  <Button
      android:id="@+id/button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Start AsyncTask"/>
</LinearLayout>

And finally we have our Activity that starts the AsyncTask and later receives the result. It's important to remember to unregister the Activity once it's destroyed, otherwise it'll be leaked as well.

public class MainActivity extends Activity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        new MyAsyncTask().execute();
      }
    });

    MyBus.getInstance().register(this);
  }

  @Override protected void onDestroy() {
    MyBus.getInstance().unregister(this);
    super.onDestroy();
  }

  @Subscribe public void onAsyncTaskResult(AsyncTaskResultEvent event) {
    Toast.makeText(this, event.getResult(), Toast.LENGTH_LONG).show();
  }
}

And that's it. The AsyncTask has no hard reference to the Activity, so there's no memory leaking. If a configuration change happens we'll automatically be notified of the result in the new Activity instance since it's registered to the event bus.

Now, you might ask: What happens if the AsyncTask finishes between onDestroy of on Activity and onCreate of the next?
While doInBackground can certainly finish at this time, onPostExecute is called by posting a message to the main thread Looper (or message queue). Since onDestroy and onCreate happens within the same message, the onPostExecute message can only be handled after both has happened - solving the issue.

Closing words
AsyncTask is not the only place where an event bus can come in handy. If you use services, it can be a hassle to get results back to the activity. An event bus solves this issue as well.

Filed under: Android Leave a comment
Comments (4) Trackbacks (0)
  1. Many Android developers do not know Java that well, since they come from mobile backgrounds rather than traditional Java application backgrounds. So many readers of this blog will not understand WHY your first AsyncTask example retains a reference to its enclosing class. Only those who not only know nested and inner classes well, even understanding how they are implemented will know why it retains such a reference. So you would improve your article if you explain why it retains it.

    This raises another point: since it is only the inner class that retains such a reference, why not, instead of bringing in a whole new family of classes in Otto, simply declare MyAsyncTask static? Then it will not retain an enclosing instance. MyAsyncTask does not actually use the fact that it is an inner class, it does not need to reference members of its enclosing class.

    • They can either take it as a fact, or look up elsewhere if they’re interested in why this is. How certain things work in java is out of scope of this post.

      It’s not static because it’s an example of how developers with limited knowledge of the Android framework uses AsyncTask. The main reason for it being used like this is when you want to change the Activity in some way based on the result of the AsyncTask.

  2. Can you share your thoughts on why you prefer using Otto in this scenario instead of local broadcasts with intents and receivers?


Leave a comment

No trackbacks yet.