Moyen idéal pour annuler une AsyncTask en cours d'exécution

108

J'exécute des opérations de récupération de fichiers audio et de lecture de fichiers audio à distance dans un thread d'arrière-plan en utilisant AsyncTask. Une Cancellablebarre de progression s'affiche pour le temps d'exécution de l'opération d'extraction.

Je veux annuler / abandonner l' AsyncTaskexécution lorsque l'utilisateur annule (décide de ne pas) l'opération. Quelle est la manière idéale de gérer un tel cas?

Samuh
la source

Réponses:

76

Juste découvert que AlertDialogs« s que boolean cancel(...);je l' ai utilisé partout ne fait rien. Génial.
Alors...

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
la source
55
au lieu de créer un indicateur booléen pour l'exécution, ne pourriez-vous pas le supprimer et le faire pendant que (! isCanceled ()) ???
confucius
36
De la documentation sur onCancelled (): "S'exécute sur le thread de l'interface utilisateur après que cancel (boolean) est appelé et que doInBackground (Object []) est terminé." Ce «après» signifie que définir un indicateur dans onCancelled et archiver doInBackground n'a aucun sens.
lopek
2
@confucius c'est vrai, mais de cette façon, le thread d'arrière-plan n'est pas interrompu, supposons le cas lors du téléchargement de l'image, le processus de téléchargement se poursuit en arrière-plan et nous n'avons pas été appelés onPostExecute.
umesh le
1
@DanHulme Je crois que j'ai fait référence à l'extrait de code fourni dans la réponse, pas au commentaire du confucius (ce qui est juste).
lopek
4
Oui, cette réponse ne fonctionne pas . Dans le doInBackground, remplacez while(running)par while(!isCancelled())comme d'autres l'ont dit ici dans les commentaires.
matt5784
76

Si vous faites des calculs :

  • Vous devez vérifier isCancelled()périodiquement.

Si vous faites une requête HTTP :

  • Enregistrez l'instance de votre HttpGetou HttpPostquelque part (par exemple, un champ public).
  • Après avoir appelé cancel, appelez request.abort(). Cela provoquera IOExceptionêtre jeté à l'intérieur de votre fichier doInBackground.

Dans mon cas, j'avais une classe de connecteur que j'ai utilisée dans diverses AsyncTasks. Pour faire simple, j'ai ajouté une nouvelle abortAllRequestsméthode à cette classe et j'ai appelé cette méthode directement après l'appel cancel.

Wrygiel
la source
merci, ça marche, mais comment éviter l'exception dans ce cas?
BegiPass
Vous devez appeler à HttpGet.abort()partir d'un fil d'arrière-plan ou vous obtiendrez un fichier android.os.NetworkOnMainThreadException.
Heath Borders
@wrygiel Si vous faites une requête HTTP, ne devriez-vous pas cancel(true)interrompre la requête? De la documentation: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 le
HttpURLConnection.disconnect();
Oded Breiner
Si vous avez une opération consommant le processeur dans AsyncTask, vous devez donc appeler cancel(true). Je l'ai utilisé et ça marche.
SMMousavi
20

Le fait est que l'appel AsyncTask.cancel () n'appelle que la fonction onCancel dans votre tâche. C'est là que vous souhaitez traiter la demande d'annulation.

Voici une petite tâche que j'utilise pour déclencher une méthode de mise à jour

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
la source
2
Cela fonctionnera, mais logiquement, lorsque vous attendez la réponse du serveur et que vous venez de faire l'opération db, cela devrait refléter les modifications correctes de votre activité. J'ai écrit un blog là-dessus, voyez ma réponse.
Vikas
4
Comme mentionné dans les commentaires sur la réponse acceptée, il n'est pas nécessaire de créer votre propre runningdrapeau. AsyncTask a un indicateur interne qui est défini lorsque la tâche a été annulée. Remplacez while (running)par while (!isCancelled()). developer.android.com/reference/android/os/... Donc, dans ce cas simple, vous n'avez pas besoin de onCancelled()remplacement.
ToolmakerSteve
11

Simple: n'utilisez pas de fichier AsyncTask. AsyncTaskest conçu pour des opérations courtes qui se terminent rapidement (dizaines de secondes) et n'ont donc pas besoin d'être annulées. "Lecture de fichier audio" n'est pas admissible. Vous n'avez même pas besoin d'un fil d'arrière-plan pour la lecture de fichiers audio ordinaires.

CommonsWare
la source
suggérez-vous que nous utilisions un thread java régulier et que nous «abandonnions» l'exécution du thread en utilisant une variable booléenne volatile - la méthode Java conventionnelle?
Samuh
34
Aucune offense Mike, mais ce n'est pas une réponse acceptable. AsyncTask a une méthode d'annulation, et cela devrait fonctionner. Autant que je sache, ce n'est pas le cas - mais même si je le fais mal, il devrait y avoir une bonne façon d'annuler une tâche. La méthode n'existerait pas autrement. Et même les tâches courtes peuvent nécessiter une annulation - j'ai une activité où elle commence une AsyncTask immédiatement lors du chargement, et si l'utilisateur répond immédiatement après l'ouverture de la tâche, il verra une fermeture forcée une seconde plus tard lorsque la tâche se termine mais pas de contexte existe pour qu'il puisse l'utiliser dans son onPostExecute.
Eric Mill
10
@Klondike: Je n'ai aucune idée de qui est "Mike". "mais ce n'est pas une réponse acceptable" - vous êtes les bienvenus à votre avis. "AsyncTask a une méthode d'annulation, et cela devrait fonctionner." - l'annulation des threads en Java est un problème depuis ~ 15 ans. Cela n'a rien à voir avec Android. En ce qui concerne votre scénario "Forcer la fermeture", cela peut être résolu par une variable booléenne, que vous testez onPostExecute()pour voir si vous devez continuer le travail.
CommonsWare
1
@Tejaswi Yerukalapudi: C'est plus qu'il ne fera rien automatiquement. Voir la réponse acceptée à cette question.
CommonsWare
10
Vous êtes censé vérifier périodiquement la méthode isCancelled dans votre doInBackground sur AsyncTask. C'est juste là dans la documentation: developer.android.com/reference/android/os/...
Christopher Perry
4

La seule façon de le faire est de vérifier la valeur de la méthode isCancelled () et d'arrêter la lecture lorsqu'elle renvoie true.

dbyrne
la source
4

C'est ainsi que j'écris mon AsyncTask
le point clé est d'ajouter 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
la source
1
J'ai trouvé que le simple fait d'appeler cancel (true) sur la tâche asynchrone et de vérifier périodiquement isCancelled () fonctionne, mais en fonction de ce que fait votre tâche, cela peut prendre jusqu'à 60 secondes avant d'être interrompu. L'ajout de Thread.sleep (1) lui permet de s'interrompre immédiatement. (La tâche asynchrone entre plutôt dans un état d'attente et n'est pas immédiatement supprimée). Merci pour cela.
John J Smith
0

Notre variable de classe globale AsyncTask

LongOperation LongOperationOdeme = new LongOperation();

Et action KEYCODE_BACK qui interrompent AsyncTask

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

Ça marche pour moi.

Göksel Güren
la source
0

Je n'aime pas forcer d'interrompre cancel(true)inutilement mes tâches asynchrones car elles peuvent avoir des ressources à libérer, telles que la fermeture de sockets ou de flux de fichiers, l'écriture de données dans la base de données locale, etc. D'autre part, j'ai été confronté à des situations dans lesquelles le La tâche async refuse de se terminer une partie du temps, par exemple parfois lorsque l'activité principale est en cours de fermeture et que je demande à la tâche asynchrone de se terminer depuis l'intérieur de la onPause()méthode de l'activité . Il ne s'agit donc pas simplement d'appeler running = false. Je dois opter pour une solution mixte: les deux appellent running = false, puis donnent à la tâche asynchrone quelques millisecondes pour terminer, puis appellent soit cancel(false)ou 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;
}

En conséquence, après les doInBackground()finitions, la onCancelled()méthode est parfois appelée, et parfois onPostExecute(). Mais au moins la fin de la tâche asynchrone est garantie.

Piovezan
la source
Ressemble à une condition de course.
msangel
0

En référence à la réponse de Yanchenko du 29 avril '10: Utiliser une approche 'while (running)' est bien lorsque votre code sous 'doInBackground' doit être exécuté plusieurs fois lors de chaque exécution de l'AsyncTask. Si votre code sous 'doInBackground' doit être exécuté une seule fois par exécution de l'AsyncTask, envelopper tout votre code sous 'doInBackground' dans une boucle 'while (running)' n'empêchera pas le code d'arrière-plan (thread d'arrière-plan) de s'exécuter lorsque le AsyncTask lui-même est annulé, car la condition 'while (running)' ne sera évaluée qu'une fois que tout le code à l'intérieur de la boucle while aura été exécuté au moins une fois. Vous devez donc soit (a.) Diviser votre code sous 'doInBackground' en plusieurs blocs 'while (running)' ou (b.) Effectuer de nombreux 'isCancelled'https://developer.android.com/reference/android/os/AsyncTask.html .

Pour l'option (a.), On peut donc modifier la réponse de Yanchenko comme suit:

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

// ...

Pour l'option (b.), Votre code dans 'doInBackground' ressemblera à ceci:

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
la source