Since AsyncTask
was introduced in Cupcake (API 3, Android 1.5) in 2009, it has been consistently promoted by the Android team as simple:
The code samples which they provide reinforce this message of simplicity, especially to those of us who have had to work with threads in more painful ways. AsyncTask
is very attractive.
Yet in the many years since, crashes, memory leaks, and other problems have plagued most developers who have chosen to use AsyncTask
in their production applications. This is often due to Activity
destruction and recreation on runtime configuration change (especially orientation/rotation) while the AsyncTask
is running doInBackground(Params...)
; when onPostExecute(Result)
is called, the Activity
has already been destroyed, leaving UI references in an unusable state (or even null
).
And the lack of obvious, clear, and concise guidance and code samples from the Android team on this issue has only made things worse, leading to confusion as well as various workarounds and hacks, some decent, some terrible:
Obviously, since AsyncTask
can be used in many situations, there is no One Way to accommodate this issue. My question, however, is about options.
What are the canonical (endorsed by the Android team) best practices, with concise code samples, for integrating AsyncTask
with the Activity
/Fragment
lifecycle and automatic restarts on runtime configuration change?
An asynchronous task is defined by a computation that runs on a background thread and whose result is published on the UI thread. An asynchronous task is defined by 3 generic types, called Params , Progress and Result , and 4 steps, called onPreExecute , doInBackground , onProgressUpdate and onPostExecute .
AsyncTask deprecated alternative Android – Java Here, we are using the Executor class as an alternative to the deprecated AsyncTask. First, create an instance of the executor by using any of the factory methods: private final Executor executor = Executors. newSingleThreadExecutor();
Using Handlers you have the advantage of MessagingQueues , so if you want to schedule messages or update multiple UI elements or have repeating tasks. AsyncTask are similar, in fact, they make use of Handler , but doesn't run in the UI thread, so it's good for fetching data, for instance fetching web services.
AsyncTask will be re-executed as background thread again, and previous background thread processing was just be redundant and zombie. AsyncTaskLoader will be just re-used basing on Loader ID that registered in Loader Manager before, so avoid re-executing network transaction.
From Memory & Threading. (Android Performance Patterns Season 5, Ep. 3):
You've got some threading object that's declared as an inner class of an
Activity
. The problem here is that theAsyncTask
object now has an implicit reference to the enclosingActivity
, and will keep that reference until the work object has been destroyed... Until this work completes, theActivity
stays around in memory... This type of pattern also leads to common types of crashes seen in Android apps...The takeaway here is that you shouldn't hold references to any types of UI-specific objects in any of your threading scenarios.
Although the documentation is sparse and scattered, the Android team have provided at least three distinct approaches to dealing with restarts on config change using AsyncTask
:
WeakReference
s to UI objectsActivity
or Fragment
using "work records"From Using AsyncTask | Processes and Threads | Android Developers
To see how you can persist your task during one of these restarts and how to properly cancel the task when the activity is destroyed, see the source code for the Shelves sample application.
In the Shelves app, references to the tasks are maintained as fields in an Activity
, so that they can be managed in the Activity
's lifecycle methods. Before taking a look at the code, however, there are a couple of important things to note.
First, this app was written before AsyncTask
was added to the platform. A class that strongly resembles what was later released as AsyncTask
is included in the source, called UserTask
. For our discussion here, UserTask
is functionally equivalent to AsyncTask
.
Second, subclasses of UserTask
are declared as inner classes of an Activity
. This approach is now regarded as an anti-pattern, as noted earlier (see Don't hold references to UI specific objects above). Fortunately, this implementation detail doesn't impact the overall approach of managing running tasks in lifecycle methods; however, if you choose to use this sample code for your own app, declare your subclasses of AsyncTask
elsewhere.
Override onDestroy()
, cancel the tasks, and set task references to null
. (I'm not sure if setting references to null
has any impact here; if you have further info, please comment and I'll update the answer accordingly).
Override AsyncTask#onCancelled(Object)
if you need to clean up or perform any other needed work after AsyncTask#doInBackground(Object[])
returns.
AddBookActivity.java
public class AddBookActivity extends Activity implements View.OnClickListener,
AdapterView.OnItemClickListener {
// ...
private SearchTask mSearchTask;
private AddTask mAddTask;
// Tasks are initialized and executed when needed
// ...
@Override
protected void onDestroy() {
super.onDestroy();
onCancelAdd();
onCancelSearch();
}
// ...
private void onCancelSearch() {
if (mSearchTask != null && mSearchTask.getStatus() == UserTask.Status.RUNNING) {
mSearchTask.cancel(true);
mSearchTask = null;
}
}
private void onCancelAdd() {
if (mAddTask != null && mAddTask.getStatus() == UserTask.Status.RUNNING) {
mAddTask.cancel(true);
mAddTask = null;
}
}
// ...
// DO NOT DECLARE YOUR TASK AS AN INNER CLASS OF AN ACTIVITY
// Instances of this class will hold an implicit reference to the enclosing
// Activity as long as the task is running, even if the Activity has been
// otherwise destroyed by the system. Declare your task where you can be
// sure it holds no implicit references to UI-specific objects (Views,
// etc.), and do not hold explicit references to them in your own
// implementation.
private class AddTask extends UserTask<String, Void, BooksStore.Book> {
// ...
@Override
public void onCancelled() {
enableSearchPanel();
hidePanel(mAddPanel, false);
}
// ...
}
// DO NOT DECLARE YOUR TASK AS AN INNER CLASS OF AN ACTIVITY
// Instances of this class will hold an implicit reference to the enclosing
// Activity as long as the task is running, even if the Activity has been
// otherwise destroyed by the system. Declare your task where you can be
// sure it holds no implicit references to UI-specific objects (Views,
// etc.), and do not hold explicit references to them in your own
// implementation.
private class SearchTask extends UserTask<String, ResultBook, Void>
implements BooksStore.BookSearchListener {
// ...
@Override
public void onCancelled() {
enableSearchPanel();
hidePanel(mSearchPanel, true);
}
// ...
}
Override onSaveInstanceState(Bundle, PersistableBundle)
, cancel the tasks, and save state about the tasks so they can be restarted when instance state is restored.
Override onRestoreInstanceState(Bundle, PersistableBundle)
, retrieve state about canceled tasks, and start new tasks with the data from the canceled task state.
AddBookActivity.java
public class AddBookActivity extends Activity implements View.OnClickListener,
AdapterView.OnItemClickListener {
// ...
private static final String STATE_ADD_IN_PROGRESS = "shelves.add.inprogress";
private static final String STATE_ADD_BOOK = "shelves.add.book";
private static final String STATE_SEARCH_IN_PROGRESS = "shelves.search.inprogress";
private static final String STATE_SEARCH_QUERY = "shelves.search.book";
// ...
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// ...
restoreAddTask(savedInstanceState);
restoreSearchTask(savedInstanceState);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (isFinishing()) {
// ...
saveAddTask(outState);
saveSearchTask(outState);
}
}
// ...
private void saveAddTask(Bundle outState) {
final AddTask task = mAddTask;
if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
final String bookId = task.getBookId();
task.cancel(true);
if (bookId != null) {
outState.putBoolean(STATE_ADD_IN_PROGRESS, true);
outState.putString(STATE_ADD_BOOK, bookId);
}
mAddTask = null;
}
}
private void restoreAddTask(Bundle savedInstanceState) {
if (savedInstanceState.getBoolean(STATE_ADD_IN_PROGRESS)) {
final String id = savedInstanceState.getString(STATE_ADD_BOOK);
if (!BooksManager.bookExists(getContentResolver(), id)) {
mAddTask = (AddTask) new AddTask().execute(id);
}
}
}
private void saveSearchTask(Bundle outState) {
final SearchTask task = mSearchTask;
if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
final String bookId = task.getQuery();
task.cancel(true);
if (bookId != null) {
outState.putBoolean(STATE_SEARCH_IN_PROGRESS, true);
outState.putString(STATE_SEARCH_QUERY, bookId);
}
mSearchTask = null;
}
}
private void restoreSearchTask(Bundle savedInstanceState) {
if (savedInstanceState.getBoolean(STATE_SEARCH_IN_PROGRESS)) {
final String query = savedInstanceState.getString(STATE_SEARCH_QUERY);
if (!TextUtils.isEmpty(query)) {
mSearchTask = (SearchTask) new SearchTask().execute(query);
}
}
}
This is a straightforward approach, and should make sense even to beginners who are just getting acquainted with the Activity
lifecycle. It also has the advantage of not requiring mimimal code outside of the task class itself, touching one to three lifecycle methods, depending on needs. A simple, 7-line onDestroy()
snippet in the Usage section of the AsyncTask
javadoc could have saved us all a lot of grief. Perhaps some future generation may be spared.
Pass UI objects as parameters in the AsyncTask
's constructor. Store weak references to these objects as WeakReference
fields in the AsyncTask
.
In onPostExecute()
, check that the UI object WeakReference
s are not null
then update them directly.
From Use an AsyncTask | Processing Bitmaps Off the UI Thread | Android Developers
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
The
WeakReference
to theImageView
ensures that theAsyncTask
does not prevent theImageView
and anything it references from being garbage collected. There’s no guarantee theImageView
is still around when the task finishes, so you must also check the reference inonPostExecute()
. TheImageView
may no longer exist, if for example, the user navigates away from the activity or if a configuration change happens before the task finishes.
This approach is simpler and tidier than the first, adding only a type change and a null check to the task class, and no additional code anywhere else.
There is a cost to this simplicity, however: the task will run to its end without being canceled on config change. If your task is expensive (CPU, memory, battery), has side effects, or needs to be automatically restarted on Activity
restart, then the first approach is probably a better option.
From Memory & Threading. (Android Performance Patterns Season 5, Ep. 3)
...force the top-level
Activity
orFragment
to be the sole system responsible for updating the UI objects.For example, when you'd like to kick off some work, create a "work record" that pairs a
View
with some update function. When that block of work is finished, it submits the results back to theActivity
using anIntent
or arunOnUiThread(Runnable)
call.The
Activity
can then call the update function with the new information, or if theView
isn't there, just drop the work altogether. And, if theActivity
that issued the work was destroyed, then the newActivity
won't have a reference to any of this, and it will just drop the work, too.
Here is a screenshot of the accompanying diagram that describes this approach:
Code samples were not provided in the video, so here is my take on a basic implementation:
WorkRecord.java
public class WorkRecord {
public static final String ACTION_UPDATE_VIEW = "WorkRecord.ACTION_UPDATE_VIEW";
public static final String EXTRA_WORK_RECORD_KEY = "WorkRecord.EXTRA_WORK_RECORD_KEY";
public static final String EXTRA_RESULT = "WorkRecord.EXTRA_RESULT";
public final int viewId;
public final Callback callback;
public WorkRecord(@IdRes int viewId, Callback callback) {
this.viewId = viewId;
this.callback = callback;
}
public interface Callback {
boolean update(View view, Object result);
}
public interface Store {
long addWorkRecord(WorkRecord workRecord);
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity implements WorkRecord.Store {
// ...
private final Map<Long, WorkRecord> workRecords = new HashMap<>();
private BroadcastReceiver workResultReceiver;
// ...
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
initWorkResultReceiver();
registerWorkResultReceiver();
}
@Override protected void onDestroy() {
super.onDestroy();
// ...
unregisterWorkResultReceiver();
}
// Initializations
private void initWorkResultReceiver() {
workResultReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
doWorkWithResult(intent);
}
};
}
// Result receiver
private void registerWorkResultReceiver() {
final IntentFilter workResultFilter = new IntentFilter(WorkRecord.ACTION_UPDATE_VIEW);
LocalBroadcastManager.getInstance(this).registerReceiver(workResultReceiver, workResultFilter);
}
private void unregisterWorkResultReceiver() {
if (workResultReceiver != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(workResultReceiver);
}
}
private void doWorkWithResult(Intent resultIntent) {
final long key = resultIntent.getLongExtra(WorkRecord.EXTRA_WORK_RECORD_KEY, -1);
if (key <= 0) {
Log.w(TAG, "doWorkWithResult: WorkRecord key not found, exiting:"
+ " intent=" + resultIntent);
return;
}
final Object result = resultIntent.getExtras().get(WorkRecord.EXTRA_RESULT);
if (result == null) {
Log.w(TAG, "doWorkWithResult: Result not found, exiting:"
+ " key=" + key
+ ", intent=" + resultIntent);
return;
}
final WorkRecord workRecord = workRecords.get(key);
if (workRecord == null) {
Log.w(TAG, "doWorkWithResult: matching WorkRecord not found, exiting:"
+ " key=" + key
+ ", workRecords=" + workRecords
+ ", result=" + result);
return;
}
final View viewToUpdate = findViewById(workRecord.viewId);
if (viewToUpdate == null) {
Log.w(TAG, "doWorkWithResult: viewToUpdate not found, exiting:"
+ " key=" + key
+ ", workRecord.viewId=" + workRecord.viewId
+ ", result=" + result);
return;
}
final boolean updated = workRecord.callback.update(viewToUpdate, result);
if (updated) workRecords.remove(key);
}
// WorkRecord.Store implementation
@Override public long addWorkRecord(WorkRecord workRecord) {
final long key = new Date().getTime();
workRecords.put(key, workRecord);
return key;
}
}
MyTask.java
public class MyTask extends AsyncTask<Void, Void, Object> {
// ...
private final Context appContext;
private final long workRecordKey;
private final Object otherNeededValues;
public MyTask(Context appContext, long workRecordKey, Object otherNeededValues) {
this.appContext = appContext;
this.workRecordKey = workRecordKey;
this.otherNeededValues = otherNeededValues;
}
// ...
@Override protected void onPostExecute(Object result) {
final Intent resultIntent = new Intent(WorkRecord.ACTION_UPDATE_VIEW);
resultIntent.putExtra(WorkRecord.EXTRA_WORK_RECORD_KEY, workRecordKey);
resultIntent.putExtra(WorkRecord.EXTRA_RESULT, result);
LocalBroadcastManager.getInstance(appContext).sendBroadcast(resultIntent);
}
}
// ...
private WorkRecord.Store workRecordStore;
private MyTask myTask;
// ...
private void initWorkRecordStore() {
// TODO: get a reference to MainActivity and check instanceof WorkRecord.Store
workRecordStore = (WorkRecord.Store) activity;
}
private void startMyTask() {
final long key = workRecordStore.addWorkRecord(key, createWorkRecord());
myTask = new MyTask(getApplicationContext(), key, otherNeededValues).execute()
}
private WorkRecord createWorkRecord() {
return new WorkRecord(R.id.view_to_update, new WorkRecord.Callback() {
@Override public void update(View view, Object result) {
// TODO: update view using result
}
});
}
Obviously, this approach is a huge effort compared to the other two, and overkill for many implementations. For larger apps that do a lot of threading work, however, this can serve as a suitable base architecture.
Implementing this approach exactly as described in the video, the task will run to its end without being canceled on config change, like the second approach above. If your task is expensive (CPU, memory, battery), has side effects, or needs to be automatically restarted on Activity
restart, then you would need to modify this approach to accommodate canceling, optionally saving and restarting, the task. Or just stick with the first approach; Romain had a clear vision for this and implemented it well.
This is a big answer, and it's likely that I have made errors and omissions. If you find any, please comment and I'll update the answer. Thanks!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With