- Asynchronous Android Programming(Second Edition)
- Helder Vasconcelos
- 2215字
- 2021-07-14 10:43:16
Common AsyncTask issues
As with any powerful programming abstraction, AsyncTask
is not entirely free from issues and compromises. In the next sections we are going to list some of the pitfalls that we could face when we want to make use of this construct in our applications.
Fragmentation issues
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 AsyncTask
s—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.
At the time of writing in October 2015, only 4% of Android devices run a version of Android in the danger zone between API Levels 4 and 10, and therefore targeting your application to Level 11 would not reduce your market reach significantly.
When the ThreadPoolExecutor
is used as the executor, the changes introduced in Lollipop (API Level 21) could also bring behavior drifts in relation to older versions (API Level >10). The modern AsyncTask's
ThreadPoolExecutor
is limited to the device's CPU cores * 2 + 1 concurrent threads, with an additional queue of 128 tasks to queue up work.
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.
Memory leaks
In cases where we keep a reference to an Activity
or a View
, we could prevent an entire tree of objects from being garbage collected when the activity is destroyed. The developer needs to make sure that it cancels the task and removes the reference to the destroyed activity or view.
Activity lifecycle issues
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. The Activity might finish for many reasons, including configuration changes caused by the user rotating the device (the Activity is destroyed and created again with a new address in the memory), the user connecting the device to a docking station, or any other kind of context change.
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.
On occasions after a device rotation, the AsyncTask
continues to be meaningful and has valid content to deliver, however, it has reference to an activity or a view that was destroyed and therefore is no longer able to update the UI and finish its work and deliver its result.
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.
Handling lifecycle issues with early cancellation
First and foremost, 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.
This solution is straightforward and clean but far from ideal because you might waste precious resources by starting over the background work again unaware that you might already have a valid result or that your AsyncTask
is still running.
Beyond that, when you start multiple AsyncTasks
and start them again when the device rotation happens, the waste grows substantially since we have to cancel and fire up the same number of tasks again.
Handling lifecycle issues with retained headless fragments
If the Activit
y is finishing because of a configuration change, it may still be useful to use the results of the background task and display them 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 23. To use Fragment
, 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
. Since they do not have a UI related to them, they do not have to be destroyed and recreated again when the user rotates the device, for example.
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 { void onPreExecute(); void onProgressUpdate(Integer... progress); void onPostExecute(Bitmap result); void onCancelled(Bitmap 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 DownloadImageHeadlessFragment extends Fragment { // Reference to the activity that receives the // async task callbacks private AsyncListener listener; private DownloadImageTask task; // Function to create new instances public static DownloadImageHeadlessFragment newInstance(String url) { DownloadImageHeadlessFragment myFragment = new DownloadImageHeadlessFragment(); Bundle args = new Bundle(); args.putString("url", url); myFragment.setArguments(args); return myFragment; } // Called to do initial creation of fragment public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); task = new DownloadImageTask(); url = new URL(getArguments().getString("url")); task.execute(url); } // Called when an activity is attached public void onAttach(Activity activity) { super.onAttach(activity); listener = (AsyncListener)activity; } public void onDetach() { super.onDetach(); listener = null; } // Cancel the download public void cancel() { if (task != null) { task.cancel(false); } } private class DownloadImageTask extends AsyncTask<URL, Integer, Bitmap> { // ... doInBackground elided for brevity ... } }
As you might know, a fragment has its lifecycle tied to its own Activity
, and therefore the callbacks are invoked in an orderly fashion following the current activity lifecycle events. For example, when the activity is stopped, all the fragments attached to it will be detached and notified of the Activity
state change.
In our example, we're using the Fragment
lifecycle methods (onAttach
and onDetach
) to save or remove the current Activity
reference in our retained fragment.
When the Activity
gets attached to our fragment, the onCreate
method is invoked to create the private DownloadImageTask
object and thereafter, the execute method is invoked to start the download in the background.
The newInstance
static method is used to initialize and setup a new fragment, without having to call its constructor and a URL setter. As soon as we create the fragment object instance, we save the image URL in the bundle object stored by the fragment arguments member, using the setArguments
function. If the Android system restores our fragment, it calls the default constructor with no arguments, and moreover it could make use of the old bundle to recreate the fragment.
Whenever the activity gets destroyed and recreated during a configuration change, the setRetainInstance(true)
forces the fragment to survive during the activity recycling transition. As you can perceive, this technique could be extremely useful in situations where we don't want to reconstruct objects that are expensive to recreate again or objects that have an independent lifecycle when an Activity is destroyed through a configuration change.
Note
It is important to know that the retainInstance()
can only be used with fragments that are not in the back stack. On retained fragments, onCreate()
and onDestroy()
are not called when the activity is re-attached to a new Activity.
Next, our Fragment
has to manage and execute a DownloadImageTask
, that proxies progress updates and results back to the Activity
via the AsyncListener
interface:
private class DownloadImageTask extends AsyncTask<URL, Integer, Bitmap> { ... protected void onPreExecute() { if (listener != null) listener.onPreExecute(); } protected void onProgressUpdate(Integer... values) { if (listener != null) listener.onProgressUpdate(values); } protected void onPostExecute(Bitmap result) { if (listener != null) listener.onPostExecute(result); } protected void onCancelled(Bitmap result) { if (listener != null) listener.onCancelled(result); } }
As described previously, the AsyncListener
, is the entity that is responsible for updating the UI with the result that will come from our background task.
Now, all we need is the host Activity that implements AsyncListener
and uses DownloadImageHeadlessFragment
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:
public class ShowMyPuppyHeadlessActivity extends FragmentActivity implements DownloadImageHeadlessFragment.AsyncListener { private static final String DOWNLOAD_PHOTO_FRAG = "download_photo_as_fragment"; .. @Override protected void onCreate(Bundle savedInstanceState) { ... FragmentManager fm = getSupportFragmentManager(); downloadFragment = (DownloadImageHeadlessFragment) fm.findFragmentByTag(DOWNLOAD_PHOTO_FRAG); // If the Fragment is non-null, then it is currently being // retained across a configuration change. if (downloadFragment == null) { downloadFragment = DownloadImageHeadlessFragment. newInstance("http://img.allw.mn/content" + "/www/2009/03/april1.jpg"); fm.beginTransaction().add(downloadFragment, DOWNLOAD_PHOTO_FRAG). commit(); } }
First, when the activity is created in the onCreate
callback, we check if the fragment already exists in FragmentManager
, and we only create the instance if it is missing.
When the fragment is created, we build a fragment instance over the newInstance
method and then we push the fragment to FragmentManager
, the entity that will store and make the transition.
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:
@Override public void onProgressUpdate(Integer... value) { if (progress == null) prepareProgressDialog(); progress.setProgress(value[0]); }
Finally, Activity
will need to implement the onPostExecute
and onCancelled
callbacks defined by AsyncListener
. The onPostExecute
will update the resultView
as in the previous examples, and both will do a little cleanup—dismissing the dialog and removing Fragment as its work is now done:
@Override public void onPostExecute(Bitmap result) { if (result != null) { ImageView iv = (ImageView) findViewById( R.id.downloadedImage); iv.setImageBitmap(result); } cleanUp(); } // When the task is cancelled the dialog is dimissed @Override public void onCancelled(Bitmap result) { cleanUp(); } // Dismiss the progress dialog and remove the // the fragment from the fragment manager private void cleanUp() { if (progress != null) { progress.dismiss(); progress = null; } FragmentManager fm = getSupportFragmentManager(); Fragment frag = fm.findFragmentByTag(DOWNLOAD_PHOTO_FRAG); fm.beginTransaction().remove(frag).commit(); }
This technique, well known in the Android development community as a headless Fragment
, is simple and consistent, since it attaches the recreated activity to the headless Fragment
each time a configuration change happens. An activity reference is maintained, on the fragment, and updated when the fragment gets attached (Activity creation) and gets detached (Activity destroyed).
Taking advantage of this pattern, the AsyncTask
never has to follow the unpredictable occurrence of configuration changes or worry about UI updates when it finishes its work because it forwards the lifecycle callbacks to the current Activity
.
- 數字媒體應用教程
- Getting started with Google Guava
- Arduino開發實戰指南:LabVIEW卷
- Visual Basic程序設計(第3版):學習指導與練習
- 精通軟件性能測試與LoadRunner實戰(第2版)
- Scratch 3.0少兒編程與邏輯思維訓練
- 精通Scrapy網絡爬蟲
- Python面向對象編程:構建游戲和GUI
- Building Serverless Applications with Python
- 用戶體驗增長:數字化·智能化·綠色化
- HTML5與CSS3基礎教程(第8版)
- Scala編程實戰
- DevOps 精要:業務視角
- 原型設計:打造成功產品的實用方法及實踐
- Clojure編程樂趣