Maneira ideal de cancelar uma AsyncTask em execução

108

Estou executando operações remotas de busca de arquivo de áudio e reprodução de arquivo de áudio em um thread de fundo usando AsyncTask. Uma Cancellablebarra de progresso é mostrada para o tempo de execução da operação de busca.

Quero cancelar / abortar a AsyncTaskexecução quando o usuário cancelar (decidir não) a operação. Qual é a forma ideal de lidar com esse caso?

Samuh
fonte

Respostas:

76

Só descobri que AlertDialogsé boolean cancel(...);que eu tenho usado em todos os lugares, na verdade, não faz nada. Ótimo.
Assim...

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
fonte
55
em vez de fazer um sinalizador booleano para execução, você não poderia removê-lo e fazer isso while (! isCanceled ()) ???
confucius
36
De docs sobre onCancelled (): "É executado no encadeamento da IU após cancel (boolean) ser invocado e doInBackground (Object []) terminar." Este 'depois' significa que definir um sinalizador em onCancelled e verificar em doInBackground não faz sentido.
lopek
2
@confucius isso mesmo, mas desta forma a thread em segundo plano não é interrompida, suponha que seja o caso ao enviar imagens, o processo de envio continua em segundo plano e não conseguimos onPostExecute chamado.
umesh
1
@DanHulme Acho que me referi ao trecho de código fornecido na resposta, não ao comentário do confucius (que está certo).
lopek
4
Sim, essa resposta não funciona . No doInBackground, substitua while(running)por while(!isCancelled())como outros disseram aqui nos comentários.
matt5784
76

Se você estiver fazendo cálculos :

  • Você tem que verificar isCancelled()periodicamente.

Se você estiver fazendo uma solicitação HTTP :

  • Salve a instância do seu HttpGetou em HttpPostalgum lugar (por exemplo, um campo público).
  • Depois de ligar cancel, ligue request.abort(). Isso fará com que IOExceptionseja jogado dentro do seu doInBackground.

No meu caso, eu tinha uma classe de conector que usei em várias AsyncTasks. Para mantê-lo simples, adicionei um novo abortAllRequestsmétodo a essa classe e chamei esse método diretamente após a chamada cancel.

wrygiel
fonte
obrigado, funciona, mas como evitar a exceção neste caso?
01 de
Você deve chamar HttpGet.abort()de um thread de segundo plano ou obterá um android.os.NetworkOnMainThreadException.
Heath Borders
@wrygiel Se você estiver fazendo uma solicitação HTTP, não deve cancel(true)interromper a solicitação? Da documentação: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 de
Se você tiver uma operação de consumo de cpu em AsyncTask, deverá chamar cancel(true). Eu usei e funciona.
SMMousavi
20

O fato é que a chamada de AsyncTask.cancel () chama apenas a função onCancel em sua tarefa. É aqui que você deseja lidar com a solicitação de cancelamento.

Aqui está uma pequena tarefa que uso para acionar um método de atualização

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
fonte
2
Isso funcionará, mas logicamente, quando você está esperando pela resposta do servidor e acabou de fazer a operação db, deve refletir as alterações corretas em sua atividade. Eu escrevi um blog sobre isso, veja minha resposta.
Vikas
4
Conforme mencionado nos comentários sobre a resposta aceita, não há necessidade de criar sua própria runningbandeira. AsyncTask tem um sinalizador interno que é definido quando a tarefa é cancelada. Substitua while (running)por while (!isCancelled()). developer.android.com/reference/android/os/… Portanto, neste caso simples, você não precisa onCancelled()substituir.
Toolmaker Steve
11

Simples: não use um AsyncTask. AsyncTaskfoi projetado para operações curtas que terminam rapidamente (dezenas de segundos) e, portanto, não precisam ser canceladas. "Reprodução de arquivo de áudio" não se qualifica. Você nem mesmo precisa de um thread de fundo para a reprodução de arquivos de áudio comuns.

CommonsWare
fonte
você está sugerindo que usemos o encadeamento java regular e "abortemos" o encadeamento executado usando a variável booleana volátil - a maneira convencional do Java?
Samuh
34
Sem ofensa Mike, mas essa não é uma resposta aceitável. AsyncTask tem um método de cancelamento e deve funcionar. Pelo que eu posso dizer, isso não acontece - mas mesmo que eu esteja fazendo errado, então deve haver uma maneira certa de cancelar uma tarefa. O método não existiria de outra forma. E mesmo tarefas curtas podem precisar de cancelamento - eu tenho uma Activity em que ela começa uma AsyncTask imediatamente após o carregamento e se o usuário responder imediatamente após abrir a tarefa, eles verão um Force Close um segundo depois quando a tarefa terminar, mas sem contexto existe para ser usado em seu onPostExecute.
Eric Mill
10
@Klondike: Não tenho ideia de quem seja "Mike". "mas essa não é uma resposta aceitável" - sua opinião é bem-vinda. "AsyncTask tem um método de cancelamento e deve funcionar." - cancelar threads em Java tem sido um problema por aproximadamente 15 anos. Não tem muito a ver com o Android. Com relação ao seu cenário "Forçar fechamento", isso pode ser resolvido por uma variável booleana, que você testa onPostExecute()para ver se deve prosseguir com o trabalho.
CommonsWare
1
@Tejaswi Yerukalapudi: É mais que ele não fará nada automaticamente. Veja a resposta aceita nesta questão.
CommonsWare
10
Você deve verificar o método isCancelled periodicamente em seu doInBackground em AsyncTask. Está bem ali nos documentos: developer.android.com/reference/android/os/…
Christopher Perry
4

A única maneira de fazer isso é verificando o valor do método isCancelled () e parando a reprodução quando ele retornar true.

dbyrne
fonte
4

É assim que escrevo minha AsyncTask,
o ponto-chave é adicionar 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
fonte
1
Descobri que simplesmente chamar cancel (true) na tarefa assíncrona e verificar isCancelled () periodicamente funciona, mas dependendo do que sua tarefa está fazendo, pode demorar até 60 segundos antes de ser interrompida. Adicionar Thread.sleep (1) permite que ele seja interrompido imediatamente. (A tarefa assíncrona entra em um estado de espera e não é descartada imediatamente). Obrigado por isso.
John J Smith,
0

Nossa variável de classe AsyncTask global

LongOperation LongOperationOdeme = new LongOperation();

E a ação KEYCODE_BACK que interrompe AsyncTask

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

Funciona para mim.

Göksel Güren
fonte
0

Não gosto de forçar a interrupção de minhas tarefas assíncronas cancel(true)desnecessariamente porque podem ter recursos a serem liberados, como fechar soquetes ou fluxos de arquivos, gravar dados no banco de dados local etc. Por outro lado, enfrentei situações em que o a tarefa assíncrona se recusa a terminar sozinha parte do tempo, por exemplo, às vezes quando a atividade principal está sendo fechada e eu solicito que a tarefa assíncrona termine de dentro do onPause()método da atividade . Portanto, não é uma questão de simplesmente ligar running = false. Tenho que buscar uma solução mista: ambos ligam running = false, depois dão à tarefa assíncrona alguns milissegundos para terminar e, em seguida, chamam 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;
}

Como resultado colateral, depois de doInBackground()terminar, às vezes o onCancelled()método é chamado e às vezes onPostExecute(). Mas pelo menos o encerramento da tarefa assíncrona é garantido.

Piovezan
fonte
Parece uma condição de corrida.
msangel
0

Com referência à resposta de Yanchenko em 29 de abril de '10: Usar uma abordagem 'while (running)' é legal quando seu código em 'doInBackground' precisa ser executado várias vezes durante cada execução da AsyncTask. Se o seu código em 'doInBackground' tiver que ser executado apenas uma vez por execução da AsyncTask, envolver todo o seu código em 'doInBackground' em um loop 'while (running)' não interromperá a execução do código de segundo plano (thread de segundo plano) quando o A própria AsyncTask é cancelada, porque a condição 'while (running)' só será avaliada depois que todo o código dentro do loop while tiver sido executado pelo menos uma vez. Você deve então (a.) Dividir seu código em 'doInBackground' em vários blocos 'while (running)' ou (b.) Executar vários 'isCancelled'https://developer.android.com/reference/android/os/AsyncTask.html .

Para a opção (a.), Pode-se modificar a resposta de Yanchenko da seguinte forma:

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 a opção (b.), Seu código em 'doInBackground' será semelhante a este:

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
fonte