AsyncTask no se detendrá incluso cuando la actividad se haya destruido

76

Tengo un objeto AsyncTask que comienza a ejecutarse cuando se crea la actividad y hace cosas en segundo plano (descarga hasta 100 imágenes). Todo funciona bien, pero hay un comportamiento peculiar que no puedo entender.

Por ejemplo: cuando cambia la orientación de la pantalla de Android, la actividad se destruye y se vuelve a crear. Así que anulo el método onRetainNonConfigurationInstance () y guardo todos los datos descargados ejecutados en AsyncTask. Mi propósito de hacer esto es que AsyncTask no se ejecute cada vez que se destruya la actividad, se cree durante los cambios de orientación, pero como puedo ver en mis registros, la AsyncTask anterior todavía se está ejecutando. (Sin embargo, los datos se guardan correctamente)

Incluso intenté cancelar AsyncTask en el método onDestroy () de la actividad, pero los registros aún muestran AsyncTask en ejecución.

Este es un comportamiento realmente extraño y realmente agradecería que alguien me dijera el procedimiento correcto para detener / cancelar AsyncTask.

Gracias

Raja
fuente

Respuestas:

144

La respuesta dada por @Romain Guy es correcta. Sin embargo, me gustaría agregar un complemento de información y dar un puntero a una biblioteca o 2 que se pueden usar para AsyncTask de larga ejecución e incluso más para asynctasks orientados a la red.

AsyncTasks ha sido diseñado para hacer cosas en segundo plano. Y sí, puedes detenerlo usando el cancelmétodo. Como descargar cosas de Internet, le recomiendo encarecidamente que se ocupa de su hilo cuando es el estado de bloqueo IO . Debe organizar su descarga de la siguiente manera:

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
}

El uso de la Thread.interruptedbandera ayudará a su hilo a salir correctamente de un estado io de bloqueo. Su hilo responderá mejor a una invocación del cancelmétodo.

Defecto de diseño de AsyncTask

Pero si su AsyncTask dura demasiado, enfrentará 2 problemas diferentes:

  1. Las actividades están mal vinculadas al ciclo de vida de la actividad y no obtendrá el resultado de su AsyncTask si su actividad muere. De hecho, sí, puede, pero será el camino más difícil.
  2. AsyncTask no está muy bien documentado. Una implementación y un uso ingenuo, aunque intuitivo, de un asynctask puede conducir rápidamente a pérdidas de memoria.

RoboSpice , la biblioteca que me gustaría presentar, utiliza un servicio en segundo plano para ejecutar este tipo de solicitudes. Ha sido diseñado para solicitudes de red. Proporciona funciones adicionales como el almacenamiento en caché automático de los resultados de las solicitudes.

Esta es la razón por la que AsyncTasks es malo para tareas de larga ejecución. El siguiente razonamiento es una adaptación de los ejercicios de las motivaciones de RoboSpice : la aplicación que explica por qué el uso de RoboSpice satisface una necesidad en la plataforma Android.

El ciclo de vida de AsyncTask y Activity

AsyncTasks no sigue el ciclo de vida de las instancias de actividad. Si inicia una AsyncTask dentro de una actividad y gira el dispositivo, la actividad se destruirá y se creará una nueva instancia. Pero AsyncTask no morirá. Seguirá viviendo hasta que se complete.

Y cuando se complete, AsyncTask no actualizará la interfaz de usuario de la nueva actividad. De hecho, actualiza la instancia anterior de la actividad que ya no se muestra. Esto puede llevar a una excepción del tipo java.lang.IllegalArgumentException: Vista no adjunta al administrador de ventanas si usa, por ejemplo, findViewById para recuperar una vista dentro de la Actividad.

Problema de pérdida de memoria

Es muy conveniente crear AsyncTasks como clases internas de sus actividades. Como AsyncTask necesitará manipular las vistas de la actividad cuando la tarea esté completa o en progreso, usar una clase interna de la actividad parece conveniente: las clases internas pueden acceder directamente a cualquier campo de la clase externa.

Sin embargo, significa que la clase interna tendrá una referencia invisible en su instancia de clase externa: la Actividad.

A largo plazo, esto produce una pérdida de memoria: si AsyncTask dura mucho, mantiene la actividad "viva" mientras que a Android le gustaría deshacerse de ella, ya que ya no se puede mostrar. La actividad no se puede recolectar como basura y ese es un mecanismo central para que Android preserve los recursos en el dispositivo.

Se perderá el progreso de su tarea

Puede utilizar algunas soluciones para crear un asynctask de ejecución prolongada y administrar su ciclo de vida de acuerdo con el ciclo de vida de la actividad. Puede cancelar AsyncTask en el método onStop de su actividad o puede dejar que su tarea asincrónica finalice y no pierda su progreso y vuelva a vincularla a la siguiente instancia de su actividad. .

Esto es posible y mostramos cómo en RobopSpice motivaciones, pero se vuelve complicado y el código no es realmente genérico. Además, aún perderá el progreso de su tarea si el usuario abandona la actividad y regresa. Este mismo problema aparece con Loaders, aunque sería un equivalente más simple a AsyncTask con la solución alternativa de vinculación mencionada anteriormente.

Usando un servicio de Android

La mejor opción es utilizar un servicio para ejecutar sus tareas en segundo plano de larga duración. Y esa es exactamente la solución propuesta por RoboSpice. Nuevamente, está diseñado para redes, pero podría extenderse a cosas no relacionadas con la red. Esta biblioteca tiene una gran cantidad de características .

Incluso puedes hacerte una idea en menos de 30 segundos gracias a una infografía .


Realmente es una muy muy mala idea usar AsyncTasks para operaciones de larga ejecución. Sin embargo, están bien para los de vida corta, como actualizar una Vista después de 1 o 2 segundos.

Te animo a que descargues la aplicación RoboSpice Motivations , que realmente explica esto en profundidad y proporciona muestras y demostraciones de las diferentes formas de hacer algunas cosas relacionadas con la red.


Si está buscando una alternativa a RoboSpice para tareas no relacionadas con la red (por ejemplo, sin almacenamiento en caché), también puede echar un vistazo a Tape .

Snicolas
fuente
1
@Snicolas, ¿puedes agregar droidQuery como alternativa a RoboSpice en la página de Github ? Lo desarrollé el año pasado y proporciona, entre otras características, la capacidad de realizar tareas de red utilizando Ajax estilo jQuery (escrito en Java Android puro).
Phil
@Phil, listo. Gracias por el enlace. Por cierto, su sintaxis está muy cerca de Ion (que vino después).
Snicolas
@Snicolas, ¡gracias! Parece que tanto droidQuery como Ion tomaron un diseño de sintaxis de Picasso . github.com/koush/ion/search?q=picasso&ref=cmdform ;)
Phil
"Realmente es una muy muy mala idea usar AsyncTasks para operaciones de larga duración. Sin embargo, están bien para operaciones de corta duración, como actualizar una Vista después de 1 o 2 segundos". . No creo que sea necesariamente cierto. La razón por la que Android sugiere esto es el hecho de que AsyncTask usa un ejecutor de grupo de subprocesos global. Simplemente puede pasar su ejecutor de grupo de subprocesos personalizado a través del método de ejecución y deshacerse de los posibles problemas de bloqueo. Aunque AsyncTask no es una panacea como mencionaste "filtraciones".
stdout
El problema no es la piscina, es realmente la fuga.
Snicolas
16

Romain Guy tiene razón. De hecho, una tarea asincrónica es responsable de terminar su propio trabajo en cualquier caso. Interrumpir no es la mejor manera, por lo que debe verificar continuamente si alguien quiere que cancele o detenga su tarea.

Digamos que AsyncTaskhaces algo en un bucle muchas veces. Entonces deberías revisar isCancelled()cada bucle.

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

doTheTask() es su trabajo real y antes de hacerlo en cada ciclo, verifica si su tarea debe cancelarse.

Generalmente debes poner una bandera en tu AsyncTaskclase o devolver un resultado apropiado de tu doInBackground()para que, en tu onPostExecute(), puedas comprobar si pudiste terminar lo que quieres o si tu trabajo fue cancelado en el medio.

Cagatay Kalan
fuente
1

Lo siguiente no resuelve su problema, pero lo previene: En el manifiesto de la aplicación, haga esto:

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

Cuando agrega esto, su actividad no se recarga en el cambio de configuración, y si desea realizar algunos cambios cuando cambia la orientación, simplemente anule el siguiente método de la actividad:

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

        //your code here
    }
Frane Poljak
fuente
o, ... este método se llamará una vez que cambie la orientación del dispositivo? @FranePoljak ... ic, ic, ...
gumuruh
1

la actividad se recrea al cambiar de orientación, sí, eso es cierto. pero puede continuar asynctask cada vez que ocurra este evento.

lo revisas

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

-salud

ralphgabb
fuente
-1

Desde el punto de vista de MVC , la actividad es el controlador ; es incorrecto que el controlador realice operaciones que sobreviven a la vista (derivada de android.view.View, generalmente solo reutiliza las clases existentes). Por lo tanto, debería ser responsabilidad del modelo iniciar AsyncTasks.

18446744073709551615
fuente
-4

Se puede utilizar class MagicAppRestarta partir de este post para matar el proceso , junto con todos los AsyncTasks; Android restaurará la pila de actividades (el usuario no mencionará nada). Es importante tener en cuenta que la única notificación antes de reiniciar un proceso es llamar onPause(); de acuerdo con la lógica del ciclo de vida de la aplicación de Android , su aplicación debe estar lista para dicha terminación de todos modos.

Lo he probado y parece funcionar. Sin embargo, por el momento planeo usar métodos "más civilizados" como referencias débiles de la clase Application (mis AsyncTasks son de corta duración y espero que no consuman tanta memoria).

Aquí hay un código con el que puedes jugar:

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));
    }
}

El resto es lo que creó Eclipse para un nuevo proyecto de Android para 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>

y una parte relevante de los registros (tenga en cuenta que solo onPausese llama ):

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
18446744073709551615
fuente
Respondido en 2013 (los tiempos de Android 2.x), votado en contra en 2017 (los tiempos de Android 6 y 7). Mucho debe haber cambiado desde entonces, sé que el hack de reinicio de la aplicación no funcionó desde Android 4.
18446744073709551615