Forma ideal de cancelar una AsyncTask en ejecución

108

Estoy ejecutando operaciones remotas de recuperación de archivos de audio y reproducción de archivos de audio en un hilo en segundo plano usando AsyncTask. Se Cancellablemuestra una barra de progreso durante el tiempo que se ejecuta la operación de búsqueda.

Quiero cancelar / abortar la AsyncTaskejecución cuando el usuario cancela (decide en contra) la operación. ¿Cuál es la forma ideal de manejar un caso así?

Samuh
fuente

Respuestas:

76

Acabo de descubrir que AlertDialogs's boolean cancel(...);He estado usando todas partes en realidad no hace nada. Excelente.
Entonces...

public class MyTask extends AsyncTask<Void, Void, Void> {

    private volatile boolean running = true;
    private final ProgressDialog progressDialog;

    public MyTask(Context ctx) {
        progressDialog = gimmeOne(ctx);

        progressDialog.setCancelable(true);
        progressDialog.setOnCancelListener(new OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                // actually could set running = false; right here, but I'll
                // stick to contract.
                cancel(true);
            }
        });

    }

    @Override
    protected void onPreExecute() {
        progressDialog.show();
    }

    @Override
    protected void onCancelled() {
        running = false;
    }

    @Override
    protected Void doInBackground(Void... params) {

        while (running) {
            // does the hard work
        }
        return null;
    }

    // ...

}
Yanchenko
fuente
55
en lugar de hacer una bandera booleana para ejecutar, ¿no podría eliminarla y hacer esto while (! isCanceled ()) ???
Confucio
36
De los documentos sobre onCancelled (): "Se ejecuta en el subproceso de la interfaz de usuario después de que se invoca cancelar (booleano) y doInBackground (Object []) ha finalizado". Este 'después' significa que configurar una bandera en onCancelled y registrar doInBackground no tiene sentido.
lopek
2
@confucius, eso es correcto, pero de esta manera el hilo de fondo no se interrumpe, supongamos que cuando se carga la imagen, el proceso de carga continúa en segundo plano y no se nos llama a onPostExecute.
umesh
1
@DanHulme Creo que me referí al fragmento de código proporcionado en la respuesta, no al comentario de confucio (que es correcto).
lopek
4
Sí, esta respuesta no funciona . En doInBackground, reemplace while(running)con while(!isCancelled())como otros han dicho aquí en los comentarios.
matt5784
76

Si está haciendo cálculos :

  • Tienes que comprobarlo isCancelled()periódicamente.

Si está haciendo una solicitud HTTP :

  • Guarde la instancia de su HttpGeto en HttpPostalgún lugar (por ejemplo, un campo público).
  • Después de llamar cancel, llame request.abort(). Esto hará que IOExceptionse arroje dentro de su doInBackground.

En mi caso, tenía una clase de conector que usé en varias AsyncTasks. Para mantenerlo simple, agregué un nuevo abortAllRequestsmétodo a esa clase y llamé a este método directamente después de llamar cancel.

wrygiel
fuente
gracias, funciona, pero ¿cómo evitar la excepción en este caso?
begiPass
Debe llamar HttpGet.abort()desde un hilo en segundo plano o obtendrá un android.os.NetworkOnMainThreadException.
Heath Borders
@wrygiel Si está haciendo una solicitud HTTP, ¿no debería cancel(true)interrumpir la solicitud? De la documentación:If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.
Storo
HttpURLConnection.disconnect();
Oded Breiner
Si tiene una operación que consume CPU en AsyncTask, entonces debe llamar cancel(true). Lo usé y funciona.
SMMousavi
20

La cuestión es que la llamada AsyncTask.cancel () solo llama a la función onCancel en su tarea. Aquí es donde desea manejar la solicitud de cancelación.

Aquí hay una pequeña tarea que utilizo para activar un método de actualización

private class UpdateTask extends AsyncTask<Void, Void, Void> {

        private boolean running = true;

        @Override
        protected void onCancelled() {
            running = false;
        }

        @Override
        protected void onProgressUpdate(Void... values) {
            super.onProgressUpdate(values);
            onUpdate();
        }

        @Override
        protected Void doInBackground(Void... params) {
             while(running) {
                 publishProgress();
             }
             return null;
        }
     }
DonCroco
fuente
2
Esto funcionará, pero lógicamente cuando está esperando la respuesta del servidor y acaba de realizar la operación de base de datos, entonces debería reflejar los cambios correctos en su actividad. Escribí un blog sobre eso, mira mi respuesta.
Vikas
4
Como se menciona en los comentarios sobre la respuesta aceptada, no es necesario crear su propia runningbandera. AsyncTask tiene un indicador interno que se establece cuando se cancela la tarea. Reemplazar while (running)con while (!isCancelled()). developer.android.com/reference/android/os/… Entonces, en este caso simple, no necesita onCancelled()anular.
ToolmakerSteve
11

Simple: no uses un AsyncTask. AsyncTaskestá diseñado para operaciones cortas que terminan rápidamente (decenas de segundos) y por lo tanto no necesitan ser canceladas. La "reproducción de archivos de audio" no califica. Ni siquiera necesita un hilo de fondo para la reproducción normal de archivos de audio.

CommonsWare
fuente
¿Está sugiriendo que usemos un hilo java regular y "abortemos" la ejecución del hilo usando una variable booleana volátil - la forma convencional de Java?
Samuh
34
No te ofendas Mike, pero esa no es una respuesta aceptable. AsyncTask tiene un método de cancelación y debería funcionar. Por lo que puedo decir, no es así, pero incluso si lo estoy haciendo mal, debería haber una forma correcta de cancelar una tarea. De lo contrario, el método no existiría. E incluso las tareas cortas pueden necesitar cancelarse: tengo una actividad en la que comienza una AsyncTask inmediatamente después de cargar, y si el usuario responde inmediatamente después de abrir la tarea, verá un cierre forzado un segundo después cuando finalice la tarea, pero sin contexto existe para que lo use en su onPostExecute.
Eric Mill
10
@Klondike: No tengo idea de quién es "Mike". "pero esa no es una respuesta aceptable" - le damos la bienvenida a su opinión. "AsyncTask tiene un método de cancelación y debería funcionar". - La cancelación de subprocesos en Java ha sido un problema durante ~ 15 años. No tiene mucho que ver con Android. Con respecto a su escenario "Forzar cierre", eso puede resolverse con una variable booleana, que prueba onPostExecute()para ver si debe seguir adelante con el trabajo.
CommonsWare
1
@Tejaswi Yerukalapudi: Es más que no hará nada automáticamente. Vea la respuesta aceptada a esta pregunta.
CommonsWare
10
Se supone que debe verificar el método isCancelled periódicamente en su doInBackground en AsyncTask. Está ahí en los documentos: developer.android.com/reference/android/os/…
Christopher Perry
4

La única forma de hacerlo es verificando el valor del método isCancelled () y deteniendo la reproducción cuando devuelve verdadero.

dbyrne
fuente
4

Así es como escribo mi AsyncTask,
el punto clave es agregar Thread.sleep (1);

@Override   protected Integer doInBackground(String... params) {

        Log.d(TAG, PRE + "url:" + params[0]);
        Log.d(TAG, PRE + "file name:" + params[1]);
        downloadPath = params[1];

        int returnCode = SUCCESS;
        FileOutputStream fos = null;
        try {
            URL url = new URL(params[0]);
            File file = new File(params[1]);
            fos = new FileOutputStream(file);

            long startTime = System.currentTimeMillis();
            URLConnection ucon = url.openConnection();
            InputStream is = ucon.getInputStream();
            BufferedInputStream bis = new BufferedInputStream(is);

            byte[] data = new byte[10240]; 
            int nFinishSize = 0;
            while( bis.read(data, 0, 10240) != -1){
                fos.write(data, 0, 10240);
                nFinishSize += 10240;
                **Thread.sleep( 1 ); // this make cancel method work**
                this.publishProgress(nFinishSize);
            }              
            data = null;    
            Log.d(TAG, "download ready in"
                  + ((System.currentTimeMillis() - startTime) / 1000)
                  + " sec");

        } catch (IOException e) {
                Log.d(TAG, PRE + "Error: " + e);
                returnCode = FAIL;
        } catch (Exception e){
                 e.printStackTrace();           
        } finally{
            try {
                if(fos != null)
                    fos.close();
            } catch (IOException e) {
                Log.d(TAG, PRE + "Error: " + e);
                e.printStackTrace();
            }
        }

        return returnCode;
    }
Andrew Chen
fuente
1
Descubrí que simplemente llamar a cancel (true) en la tarea asíncrona y verificar isCancelled () periódicamente funciona, pero dependiendo de lo que esté haciendo su tarea, puede tomar hasta 60 segundos antes de que se interrumpa. Agregar Thread.sleep (1) permite que se interrumpa inmediatamente. (Async Task pasa a un estado de espera y no se descarta inmediatamente). Gracias por esto.
John J Smith
0

Nuestra variable de clase global AsyncTask

LongOperation LongOperationOdeme = new LongOperation();

Y la acción KEYCODE_BACK que interrumpe AsyncTask

   @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            LongOperationOdeme.cancel(true);
        }
        return super.onKeyDown(keyCode, event);
    }

Esto funciona para mi.

Göksel Güren
fuente
0

No me gusta forzar la interrupción de mis tareas asíncronas cancel(true)innecesariamente porque pueden tener recursos para liberar, como cerrar sockets o flujos de archivos, escribir datos en la base de datos local, etc. Por otro lado, me he enfrentado a situaciones en las que el La tarea asíncrona se niega a terminar parte del tiempo, por ejemplo, a veces cuando se cierra la actividad principal y solicito que la tarea asíncrona termine desde dentro del onPause()método de la actividad . Así que no se trata simplemente de llamar running = false. Tengo que buscar una solución mixta: ambos llaman running = false, luego dan a la tarea asíncrona unos milisegundos para que finalice, y luego llaman cancel(false)o cancel(true).

if (backgroundTask != null) {
    backgroundTask.requestTermination();
    try {
        Thread.sleep((int)(0.5 * 1000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    if (backgroundTask.getStatus() != AsyncTask.Status.FINISHED) {
        backgroundTask.cancel(false);
    }
    backgroundTask = null;
}

Como resultado secundario, después de doInBackground()finalizar, a veces onCancelled()se llama al método y, a veces onPostExecute(). Pero al menos se garantiza la terminación de la tarea asíncrona.

Piovezan
fuente
Parece una condición de carrera.
msangel
0

Con referencia a la respuesta de Yanchenko el 29 de abril de '10: Usar un enfoque 'while (running)' es bueno cuando su código bajo 'doInBackground' debe ejecutarse varias veces durante cada ejecución de AsyncTask. Si su código bajo 'doInBackground' tiene que ejecutarse solo una vez por ejecución de AsyncTask, envolver todo su código bajo 'doInBackground' en un ciclo 'while (running)' no detendrá la ejecución del código de fondo (subproceso de fondo) cuando el AsyncTask en sí se cancela, porque la condición 'while (en ejecución)' solo se evaluará una vez que todo el código dentro del ciclo while se haya ejecutado al menos una vez. Por lo tanto, debe (a.) Dividir su código en 'doInBackground' en múltiples bloques 'while (running)' o (b.) Realizar numerosos 'isCancelled'https://developer.android.com/reference/android/os/AsyncTask.html .

Para la opción (a.), Uno puede modificar la respuesta de Yanchenko de la siguiente manera:

public class MyTask extends AsyncTask<Void, Void, Void> {

private volatile boolean running = true;

//...

@Override
protected void onCancelled() {
    running = false;
}

@Override
protected Void doInBackground(Void... params) {

    // does the hard work

    while (running) {
        // part 1 of the hard work
    }

    while (running) {
        // part 2 of the hard work
    }

    // ...

    while (running) {
        // part x of the hard work
    }
    return null;
}

// ...

Para la opción (b.), Su código en 'doInBackground' se verá así:

public class MyTask extends AsyncTask<Void, Void, Void> {

//...

@Override
protected Void doInBackground(Void... params) {

    // part 1 of the hard work
    // ...
    if (isCancelled()) {return null;}

    // part 2 of the hard work
    // ...
    if (isCancelled()) {return null;}

    // ...

    // part x of the hard work
    // ...
    if (isCancelled()) {return null;}
}

// ...
Alex Ivan Howard
fuente