As with any powerful programming abstraction, AsyncTask
is not entirely free from issues and compromises.
In the Controlling the level of concurrency section, we saw how AsyncTask
has evolved with new releases of the Android platform, resulting in behavior that varies with the platform of the device running the task, which is a part of the wider issue of fragmentation.
The simple fact is that if we target a broad range of API levels, the execution characteristics of our AsyncTasks—and therefore, the behavior of our apps—can vary considerably on different devices. So what can we do to reduce the likelihood of encountering AsyncTask
issues due to fragmentation?
The most obvious approach is to deliberately target devices running at least Honeycomb, by setting a minSdkVersion
of 11 in the Android Manifest file. This neatly puts us in the category of devices, which, by default, execute AsyncTasks serially, and therefore, much more predictably.
However, this significantly reduces the market reach of our apps. At the time of writing in September 2013, more than 34 percent of Android devices in the wild run a version of Android in the danger zone between API levels 4 and 10.
A second option is to design our code carefully and test exhaustively on a range of devices—always commendable practices of course, but as we've seen, concurrent programming is hard enough without the added complexity of fragmentation, and invariably, subtle bugs will remain.
A third solution that has been suggested by the Android development community is to reimplement AsyncTask
in a package within your own project, then extend your own AsyncTask
class instead of the SDK version. In this way, you are no longer at the mercy of the user's device platform, and can regain control of your AsyncTasks. Since the source code for AsyncTask
is readily available, this is not difficult to do.
Having deliberately moved any long-running tasks off the main thread, we've made our applications nice and responsive—the main thread is free to respond very quickly to any user interaction.
Unfortunately, we have also created a potential problem for ourselves, because the main thread is able to finish the Activity before our background tasks complete. Activity might finish for many reasons, including configuration changes caused the by the user rotating the device (the default behavior of Activity
on a change in orientation is to restart with an entirely new instance of the activity).
If we continue processing a background task after the Activity has finished, we are probably doing unnecessary work, and therefore wasting CPU and other resources (including battery life), which could be put to better use.
Also, any object references held by the AsyncTask
will not be eligible for garbage collection until the task explicitly nulls those references or completes and is itself eligible for GC (garbage collection). Since our AsyncTask
probably references the Activity or parts of the View
hierarchy, we can easily leak a significant amount of memory in this way.
A common usage of AsyncTask
is to declare it as an anonymous inner class of the host Activity
, which creates an implicit reference to the Activity and an even bigger memory leak.
There are two approaches for preventing these resource wastage problems.
First, and most obviously, we can synchronize our AsyncTask
lifecycle with that of the Activity by canceling running tasks when our Activity is finishing.
When an Activity finishes, its lifecycle callback methods are invoked on the main thread. We can check to see why the lifecycle method is being called, and if the Activity is finishing, cancel the background tasks. The most appropriate Activity lifecycle method for this is onPause
, which is guaranteed to be called before the Activity finishes.
protected void onPause() { super.onPause(); if ((task != null) && (isFinishing())) task.cancel(false); }
If the Activity is not finishing—say because it has started another Activity
and is still on the back stack—we might simply allow our background task to continue to completion.
If the Activity is finishing because of a configuration change, it may still be useful to complete the background task and display the results in the restarted Activity
. One pattern for achieving this is through the use of retained Fragments.
Fragments were introduced to Android at API level 11, but are available through a support library to applications targeting earlier API levels. All of the downloadable examples use the support library, and target API levels 7 through 19. To use Fragments, our Activity
must extend the FragmentActivity
class.
The Fragment
lifecycle is closely bound to that of the host Activity
, and a fragment will normally be disposed when the activity restarts. However, we can explicitly prevent this by invoking setRetainInstance(true)
on our Fragment
so that it survives across Activity
restarts.
Typically, a Fragment
will be responsible for creating and managing at least a portion of the user interface of an Activity, but this is not mandatory. A Fragment
that does not manage a view of its own is known as a headless Fragment
.
Isolating our AsyncTask
in a retained headless Fragment
makes it less likely that we will accidentally leak references to objects such as the View
hierarchy, because the AsyncTask
will no longer directly interact with the user interface. To demonstrate this, we'll start by defining an interface that our Activity
will implement:
public interface AsyncListener<Progress, Result> { void onPreExecute(); void onProgressUpdate(Progress... progress); void onPostExecute(Result result); void onCancelled(Result result); }
Next, we'll create a retained headless Fragment
, which wraps our AsyncTask
. For brevity, doInBackground
is omitted, as it is unchanged from the previous examples—see the downloadable samples for the complete code.
public class PrimesFragment extends Fragment { private AsyncListener<Integer,BigInteger> listener; private PrimesTask task; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); task = new PrimesTask(); task.execute(2000); } public void onAttach(Activity activity) { super.onAttach(activity); listener = (AsyncListener<Integer,BigInteger>)activity; } public void onDetach() { super.onDetach(); listener = null; } class PrimesTask extends AsyncTask<Integer, Integer, BigInteger>{ protected void onPreExecute() { if (listener != null) listener.onPreExecute(); } protected void onProgressUpdate(Integer... values) { if (listener != null) listener.onProgressUpdate(values); } protected void onPostExecute(BigInteger result) { if (listener != null) listener.onPostExecute(result); } protected void onCancelled(BigInteger result) { if (listener != null) listener.onCancelled(result); } // … doInBackground elided for brevity … } }
We're using the Fragment
lifecycle methods (onAttach
and onDetach
) to add or remove the current Activity
as a listener, and PrimesTask
delegates directly to it from all of its main-thread callbacks.
Now, all we need is the host Activity
that implements AsyncListener
and uses PrimesFragment
to implement its long-running task. The full source code is available to download from the Packt Publishing website, so we'll just take a look at the highlights.
First, the code in the button's OnClickListener
now checks to see if Fragment
already exists, and only creates one if it is missing:
FragmentManager fm = getSupportFragmentManager(); PrimesFragment primes = (PrimesFragment)fm.findFragmentByTag("primes"); if (primes == null) { primes = new PrimesFragment(); FragmentTransaction transaction = fm.beginTransaction(); transaction.add(primes, "primes").commit(); }
If our Activity
has been restarted, it will need to re-display the progress dialog when a progress update callback is received, so we check and show it, if necessary, before updating the progress bar:
public void onProgressUpdate(Integer... progress) { if (dialog == null) prepareProgressDialog(); progress.setProgress(progress[0]); }
Finally, Activity
will need to implement the onPostExecute
and onCancelled
callbacks defined by AsyncListener
. Both methods will update the resultView
as in the previous examples, then do a little cleanup—dismissing the dialog and removing Fragment
as its work is now done:
public void onPostExecute(BigInteger result) { resultView.setText(result.toString()); cleanUp(); } public void onCancelled(BigInteger result) { resultView.setText("cancelled at " + result); cleanUp(); } private void cleanUp() { dialog.dismiss(); dialog = null; FragmentManager fm = getSupportFragmentManager(); Fragment primes = fm.findFragmentByTag("primes"); fm.beginTransaction().remove(primes).commit(); }
18.116.86.255