Friday 10 November 2017

AsyncTasks done right

As an Android developer you’ve probably already used AsyncTasks to perform long-running tasks in a separate thread and deliver the results back to the main Activity.
AsyncTask are particularly well suited for Activities, because they have coordinated callback methods that run:

  • in a separate thread (doInBackground), to perform long-running operations;
  • in the main thread (onPostExecute, onProgressUpdate, etc.) to display the results in the UI.

To be more precise, AsyncTasks are generally used for tasks that don’t last too much and are related to the content of an Activity (for example, saving the data provided by the user in a local database).

For long-running operations it is always better to use a Service, because the lifecycle of a Service is not so much affected by the lifecycle of the Activity (what happens if the user closes the app while a background thread is ongoing?).
Anyway, you’ve probably used AsyncTasks in a way like this:
public class MyActivity extends AppCompatActivity {
    
    // ...
    new MyAsincTask().execute(...);
    
    private class MyAsincTask extends AsyncTask {
        @Override
        protected Integer doInBackground(Integer... integers) {
            // long running task
        }

        @Override
        protected void onPostExecute(Integer integer) {
            // display results in MyActivity
        }
    }
}

Within Android Studio Lint gives you a warning, saying that non-static inner and anonymous classes could cause memory leaks.
What’s the problem exactly? Well, in Java non-static inner and anonymous classes hold an implicit reference to the outer class. In our example, the AsyncTask holds an implicit reference to the containing Activity. If we launch an AsyncTask to do some work in the background and, while the operation is ongoing, the user closes the app, the AsyncTask keeps an implicit reference to the Activity: this reference can’t be garbage collected, thus creating a memory leak.

To solve this problem we have to convert our AsyncTask in a static class. Static classes in Java are top-level classes and they don’t hold any implicit reference to the containing Activity.
This technique, however, raises another problem. When the AsyncTask has completed the background task and the method onPostExecute is invoked, the results must be delivered back to the Activity and displayed to the user: our AsyncTask needs a reference to the Activity (maybe we need a Context to start another Activity, or some widgets to display the results). If we use a regular (strong) reference, memory leaks may occur. Moreover, what happes if the Activity no longer exists because the user has closed the app?

The solution is using weak references. JVM ignores weak references. That means objects which have only week references are eligible for garbage collection. They are likely to be garbage collected when JVM runs the garbage collector thread. JVM doesn’t show any regard for weak references.
Using weak references doesn’t prevent the garbage collector from deleting unnecessary objects in memory. In the method onPostExecute, by invoking get() on the weak references, we check if the objects are still in memory or have already been garbage collected. Only in the first case we can display the results to the connected Activity.
Have a look at the following example (the parts of interest are highlighted):
public static class GetScheduledRecordingsTask extends AsyncTask> {
        @Inject
        DBHelper dbHelper;

        private final WeakReference weakFragment;
        private final WeakReference weakCalendarView;
        private final WeakReference weakCompactCalendarViewListener;

        private final Date selectedDate;

        public GetScheduledRecordingsTask(ScheduledRecordingsFragment scheduledRecordingsFragment, CompactCalendarView compactCalendarView, CompactCalendarView.CompactCalendarViewListener compactCalendarViewListener, Date selectedDate) {
            App.getComponent().inject(this);
            weakFragment = new WeakReference<>(scheduledRecordingsFragment);
            weakCalendarView = new WeakReference<>(compactCalendarView);
            weakCompactCalendarViewListener = new WeakReference<>(compactCalendarViewListener);
            this.selectedDate = selectedDate;
        }

        protected List doInBackground(Void... params) {
            return dbHelper.getAllScheduledRecordings();
        }

        protected void onPostExecute(List scheduledRecordings) {
            ScheduledRecordingsFragment scheduledRecordingsFragment = weakFragment.get();
            CompactCalendarView calendarView = weakCalendarView.get();
            CompactCalendarView.CompactCalendarViewListener compactCalendarViewListener = weakCompactCalendarViewListener.get();
            if (scheduledRecordingsFragment == null || calendarView == null || compactCalendarViewListener == null)
                return;

            calendarView.removeAllEvents();
            for (ScheduledRecordingItem item : scheduledRecordings) {
                Event event = new Event(ContextCompat.getColor(scheduledRecordingsFragment.getActivity(), R.color.accent), item.getStart(), item);
                calendarView.addEvent(event, false);
            }
            calendarView.invalidate(); // refresh the calendar view
            compactCalendarViewListener.onDayClick(selectedDate); // click to show current day
        }
    }

// ...

new GetScheduledRecordingsTask(scheduledRecordingsFragment, calendarView, compactCalendarViewListener, selectedDate).execute();

3 comments:

  1. You should have implemented some form of task cancellation, not just flat out ignoring the results.

    ReplyDelete
  2. Hello There. I found your blog using msn. This is an extremely well written article. I’ll be sure to bookmark it and return to read more of your useful info. Thanks for the post. I’ll definitely comeback. apple support berlin

    ReplyDelete