Provided recommendation (for any approach)
Don't hold references to UI-specific objects
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 the AsyncTask
object now has an implicit reference to the enclosing Activity
, and will keep that reference until the work object has been destroyed... Until this work completes, the Activity
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.
Provided approaches
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
:
- Cancel/save/restart the running task in lifecycle methods
- Use
WeakReference
s to UI objects
- Manage in the top-level
Activity
or Fragment
using "work records"
1. Cancel/save/restart the running task in lifecycle methods
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.
Canceling the task
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);
}
// ...
}
Saving and restarting the task
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.
2. Use WeakReferences to UI objects
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 the ImageView
ensures that the AsyncTask
does not prevent the ImageView
and anything it references from being garbage collected. There’s no guarantee the ImageView
is still around when the task finishes, so you must also check the reference in onPostExecute()
. The ImageView
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.
3. Manage in the top-level Activity or Fragment using "work records"
From Memory & Threading. (Android Performance Patterns Season 5, Ep. 3)
...force the top-level Activity
or Fragment
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 the Activity
using an Intent
or a runOnUiThread(Runnable)
call.
The Activity
can then call the update function with the new information, or if the View
isn't there, just drop the work altogether. And, if the Activity
that issued the work was destroyed, then the new Activity
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);
}
}
(the class where you initiate the task)
// ...
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.
Corrections
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!