开发者

AsyncTask won't stop even when the Activity has destroyed

开发者 https://www.devze.com 2022-12-25 01:37 出处:网络
I have an AsyncTask object which starts executing when the Activity is created and does stuff in the background (downloads up to 100 images). Everything works fine but there is this peculiar behavior

I have an AsyncTask object which starts executing when the Activity is created and does stuff in the background (downloads up to 100 images). Everything works fine but there is this peculiar behavior which i'm not able to understand.

For eg: when the android screen's orientati开发者_高级运维on changes then the Activity is destroyed and created again. So I override the onRetainNonConfigurationInstance() method and save all the downloaded data executed in the AsyncTask. My purpose of doing this is to not have AsyncTask run each time Activity is destroyed-created during orientation changes, but as i can see in my logs the previous AsyncTask is still executing. (The data is saved correctly though)

I even tried to cancel the AsyncTask in the onDestroy() method of the activity but the logs still show AsyncTask as running.

This is really strange behavior and would really be thankful if someone can tell me the correct procedure to stop/cancel the AsyncTask.


The answer given by @Romain Guy is correct. Nevertheless, I would like to add a complement of information and give a pointer to a library or 2 that can be used for long running AsyncTask and even more for network oriented asynctasks.

AsyncTasks have been designed for doing stuff in background. And yes, you can stop it using the cancel method. As you download stuff from the Internet, I strongly suggest you take care of your thread when it is the IO blocking state. You should organize your download as follow :

public void download() {
    //get the InputStream from HttpUrlConnection or any other
    //network related stuff
    while( inputStream.read(buffer) != -1 && !Thread.interrupted() ) {
      //copy data to your destination, a file for instance
    }
    //close the stream and other resources
}

Using the Thread.interrupted flag will help your thread to quit properly a blocking io state. Your thread will be more responsive to an invocation of the cancel method.

AsyncTask design flaw

But if your AsyncTask lasts for too long, then you will face 2 different issues :

  1. Activities are poorly tied to the activity life cycle and you won't get the result of your AsyncTask if your activity dies. Indeed, yes, you can but it will be the rough way.
  2. AsyncTask are not very well documented. A naive, though intuitive, implementation and use of an asynctask can quickly lead to memory leaks.

RoboSpice, the library I would like to introduce, uses a background service to execute this kind of requests. It has been designed for network requests. It provides additional features such as automatic caching of requests' results.

Here is the reason why AsyncTasks are bad for long running tasks. The following reasonning is an adaptation from exerpts of RoboSpice motivations : the app that explains why using RoboSpice is filling a need on the Android platform.

The AsyncTask and Activity life cycle

AsyncTasks don't follow Activity instances' life cycle. If you start an AsyncTask inside an Activity and you rotate the device, the Activity will be destroyed and a new instance will be created. But the AsyncTask will not die. It will go on living until it completes.

And when it completes, the AsyncTask won't update the UI of the new Activity. Indeed it updates the former instance of the activity that is not displayed anymore. This can lead to an Exception of the type java.lang.IllegalArgumentException: View not attached to window manager if you use, for instance, findViewById to retrieve a view inside the Activity.

Memory leak issue

It is very convenient to create AsyncTasks as inner classes of your Activities. As the AsyncTask will need to manipulate the views of the Activity when the task is complete or in progress, using an inner class of the Activity seems convenient : inner classes can access directly any field of the outer class.

Nevertheless, it means the inner class will hold an invisible reference on its outer class instance : the Activity.

On the long run, this produces a memory leak : if the AsyncTask lasts for long, it keeps the activity "alive" whereas Android would like to get rid of it as it can no longer be displayed. The activity can't be garbage collected and that's a central mechanism for Android to preserve resources on the device.

Progress of your task will be lost

You can use some workarounds to create a long running asynctask and manage its life cycle accordingly to the life cycle of the activity. You can either cancel the AsyncTask in the onStop method of your activity or you can let your async task finish, and not loose its progress and relink it to the next instance of your activity.

This is possible and we show how in RobopSpice motivations, but it becomes complicated and the code is not really generic. Moreover, you will still loose the progress of your task if the user leaves the activity and comes back. This same issue appears with Loaders, although it would be a simpler equivalent to the AsyncTask with relinking workaround mentionned above.

Using an Android service

The best option is to use a service to execute your long running background tasks. And that is exactly the solution proposed by RoboSpice. Again, it is designed for networking but could be extended to non-network related stuff. This library has a large number of features.

You can even get an idea of it in less than 30 seconds thanks to an infographics.


It is really a very very bad idea to use AsyncTasks for long running operations. Nevertheless, they are fine for short living ones such as updating a View after 1 or 2 seconds.

I encourage you to download the RoboSpice Motivations app, it really explains this in-depth and provides samples and demonstrations of the different ways to do some network related stuff.


If you are looking for an alternative to RoboSpice for non network related tasks (for instance without caching), you could also have a look at Tape.


Romain Guy is right. In fact, an async task is responsible for finishing its own job in any case. Interrupt is not the best way, so you should continuously check if somebody wants you to cancel or stop your task.

Let's say your AsyncTask does something in a loop many times. Then you should check isCancelled() in every loop.

while ( true ) {
    if ( isCancelled())
        break;
    doTheTask();
}

doTheTask() is your real job and before you do it in every loop, you check if your task should be cancelled.

Generally you should set a flag in your AsyncTask class or return an appropriate result from your doInBackground() so that, in your onPostExecute(), you can check if you could finish what you want or if your work was cancelled in the middle.


The following doesn't solve your problem, but prevents it: In the app manifest do this:

    <activity
        android:name=".(your activity name)"
        android:label="@string/app_name"
        android:configChanges="orientation|keyboardHidden|screenSize" > //this line here
    </activity>

When you add this, your activity doesn't reload on configuration change, and if you want to make some changes when orientation changes you just override the following method of the activity:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

        //your code here
    }


the activity is recreated on orientation change, yes thats true. but you can continue you asynctask whenever this event happen.

you check it on

@Override
protected void onCreate(Bundle savedInstanceState) { 
     if ( savedInstanceState == null ) {
           startAsyncTask()
     } else {
           // ** Do Nothing async task will just continue.
     }
}

-cheers


From the MVC viewpoint, Activity is the Controller; it is wrong for the Controller to perform operations that outlive the View (derived from android.view.View, usually you just reuse the existing classes). Therefore, it should be the Model's responsibility to start AsyncTasks.


You can use class MagicAppRestart from this post to kill the process along with all AsyncTasks; Android will restore the activity stack (the user will not mention anything). It is important to note that the only notification before a process restart is calling onPause(); according to the Android app lifecycle logic, your application must be ready to such termination anyway.

I have tried it, and it seems to work. Nevertheless, at the the moment I plan to use "more civilized" methods like weak references from the Application class (my AsyncTasks are rather short-living and hopefully not so much memory-consuming).

Here is some code you can play with:

MagicAppRestart.java

package com.xyz;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

/** This activity shows nothing; instead, it restarts the android process */
public class MagicAppRestart extends Activity {
    // Do not forget to add it to AndroidManifest.xml
    // <activity android:name="your.package.name.MagicAppRestart"/>
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.exit(0);
    }
    public static void doRestart(Activity anyActivity) {
        anyActivity.startActivity(new Intent(anyActivity.getApplicationContext(), MagicAppRestart.class));
    }
}

The rest is what Eclipse created for a new Android project for com.xyz.AsyncTaskTestActivity:

AsyncTaskTestActivity.java

package com.xyz;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class AsyncTaskTestActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d("~~~~","~~~onCreate ~~~ "+this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
    public void onStartButton(View view) {
        Log.d("~~~~","~~~onStartButton {");
        class MyTask extends AsyncTask<Void, Void, Void> {

            @Override
            protected Void doInBackground(Void... params) {
                // TODO Auto-generated method stub
                Log.d("~~~~","~~~doInBackground started");
                try {
                    for (int i=0; i<10; i++) {
                        Log.d("~~~~","~~~sleep#"+i);
                        Thread.sleep(200);
                    }
                    Log.d("~~~~","~~~sleeping over");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                Log.d("~~~~","~~~doInBackground ended");
                return null;
            }
            @Override
            protected void onPostExecute(Void result) {
                super.onPostExecute(result);
                taskDone();
            }
        }
        MyTask task = new MyTask();
        task.execute(null);
        Log.d("~~~~","~~~onStartButton }");
    }
    private void taskDone() {
        Log.d("~~~~","\n\n~~~taskDone ~~~ "+this+"\n\n");
    }
    public void onStopButton(View view) {
        Log.d("~~~~","~~~onStopButton {");
        MagicAppRestart.doRestart(this);
        Log.d("~~~~","~~~onStopButton }");
    }
    public void onPause() {   Log.d("~~~~","~~~onPause ~~~ "+this);   super.onPause(); }
    public void onStop() {    Log.d("~~~~","~~~onStop ~~~ "+this);    super.onPause(); }
    public void onDestroy() { Log.d("~~~~","~~~onDestroy ~~~ "+this); super.onDestroy(); }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button android:text="Start" android:onClick="onStartButton" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
    <Button android:text="Stop" android:onClick="onStopButton" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />

</LinearLayout>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.xyz"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="7" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".AsyncTaskTestActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MagicAppRestart"/>
    </application>
</manifest>

and a relevant part of the logs (note that only onPause is called):

D/~~~~    (13667): ~~~onStartButton {
D/~~~~    (13667): ~~~onStartButton }
D/~~~~    (13667): ~~~doInBackground started
D/~~~~    (13667): ~~~sleep#0
D/~~~~    (13667): ~~~sleep#1
D/~~~~    (13667): ~~~sleep#2
D/~~~~    (13667): ~~~sleep#3
D/~~~~    (13667): ~~~sleep#4
D/~~~~    (13667): ~~~sleep#5
D/~~~~    (13667): ~~~sleep#6
D/~~~~    (13667): ~~~sleep#7
D/~~~~    (13667): ~~~sleep#8
D/~~~~    (13667): ~~~sleep#9
D/~~~~    (13667): ~~~sleeping over
D/~~~~    (13667): ~~~doInBackground ended
D/~~~~    (13667): 
D/~~~~    (13667): 
D/~~~~    (13667): ~~~taskDone ~~~ com.xyz.AsyncTaskTestActivity@40516988
D/~~~~    (13667): 




D/~~~~    (13667): ~~~onStartButton {
D/~~~~    (13667): ~~~onStartButton }
D/~~~~    (13667): ~~~doInBackground started
D/~~~~    (13667): ~~~sleep#0
D/~~~~    (13667): ~~~sleep#1
D/~~~~    (13667): ~~~sleep#2
D/~~~~    (13667): ~~~sleep#3
D/~~~~    (13667): ~~~sleep#4
D/~~~~    (13667): ~~~sleep#5
D/~~~~    (13667): ~~~onStopButton {
I/ActivityManager(   81): Starting: Intent { cmp=com.xyz/.MagicAppRestart } from pid 13667
D/~~~~    (13667): ~~~onStopButton }
D/~~~~    (13667): ~~~onPause ~~~ com.xyz.AsyncTaskTestActivity@40516988
I/ActivityManager(   81): Process com.xyz (pid 13667) has died.
I/WindowManager(   81): WIN DEATH: Window{4073ceb8 com.xyz/com.xyz.AsyncTaskTestActivity paused=false}
I/ActivityManager(   81): Start proc com.xyz for activity com.xyz/.AsyncTaskTestActivity: pid=13698 uid=10101 gids={}
I/ActivityManager(   81): Displayed com.xyz/.AsyncTaskTestActivity: +44ms (total +65ms)
D/~~~~    (13698): ~~~onCreate ~~~ com.xyz.AsyncTaskTestActivity@40517238
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号