官术网_书友最值得收藏!

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 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.

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 Activity 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.

主站蜘蛛池模板: 巴中市| 同江市| 利辛县| 山东| 崇明县| 达拉特旗| 林西县| 饶河县| 大洼县| 太仆寺旗| 三江| 博客| 忻城县| 东至县| 承德市| 大关县| 平远县| 铅山县| 星座| 新干县| 康保县| 小金县| 双城市| 林周县| 靖安县| 古丈县| 西贡区| 莒南县| 化州市| 牟定县| 任丘市| 扎兰屯市| 边坝县| 本溪| 屏山县| 伽师县| 临沭县| 长乐市| 阳西县| 阳朔县| 南溪县|