取消执行中的AsyncTask的理想方式

109
我正在使用AsyncTask在后台线程中执行远程音频文件获取和音频文件播放操作。当获取操作运行时,会显示可取消的进度条。
当用户取消(决定不进行)操作时,我想取消/中止AsyncTask运行。处理这种情况的理想方式是什么?
9个回答

77

刚刚发现我到处都在使用的 AlertDialogsboolean cancel(...); 其实什么也没做。太棒了。
那么...

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

    // ...

}

55
能否改为 while(!isCanceled()),而不是创建一个布尔标志来运行代码? - confucius
36
在有关 onCancelled() 的文档中,“Runs on the UI thread after cancel(boolean) is invoked and doInBackground(Object[]) has finished.”这个“after”意味着在onCancelled中设置标志并在doInBackground中进行检查是没有意义的。 - lopek
2
@confucius 没错,但这种方式不会中断后台线程。比如上传图片时,上传过程在后台继续进行,我们就无法调用 onPostExecute 方法。 - umesh
1
@DanHulme 我相信我所提到的是答案中提供的代码片段,而不是孔子的评论(这是正确的)。 - lopek
4
是的,这个答案行不通。在doInBackground中,将while(running)替换为while(!isCancelled()),就像其他人在评论中说的那样,但不要改变原来的意思。 - matt5784
显示剩余6条评论

76

如果你正在进行计算:

  • 您必须定期检查isCancelled()

如果您正在进行HTTP请求:

  • 将您的 HttpGetHttpPost 的实例保存在某个地方(例如公共字段)。
  • 在调用cancel之后,调用request.abort()。 这将导致IOExceptiondoInBackground中被抛出。

在我的情况下,我有一个连接器类,我在各种AsyncTasks中使用它。为了保持简单,我向该类添加了一个新的abortAllRequests方法,并在调用cancel之后直接调用此方法。


谢谢,它有效,但在这种情况下如何避免异常? - begiPass
你必须在后台线程中调用 HttpGet.abort(),否则会出现 android.os.NetworkOnMainThreadException 异常。 - Heath Borders
如果你正在进行一个HTTP请求,cancel(true)不应该中断请求吗?根据文档:如果任务已经开始,则mayInterruptIfRunning参数确定执行此任务的线程是否应该被中断以尝试停止任务。 - Storo
HttpURLConnection.disconnect(); - Oded Breiner
如果你在AsyncTask中有一个占用CPU的操作,那么你必须调用cancel(true)。我使用了它并且它有效。 - S.M.Mousavi
request.abort() 是什么? - user25

20

问题在于AsyncTask.cancel()方法只会调用你的任务中的onCancel函数。这就是你想要处理取消请求的地方。

下面是一个我用来触发更新方法的小任务。

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

2
这会起作用,但从逻辑上讲,当您等待服务器响应时,并且刚刚执行了数据库操作,则应正确地反映更改到您的活动中。我在博客中写了一篇关于此的文章,请查看我的答案。 - Vikas
4
如已接受答案评论中提到,不需要创建自己的 running 标志。AsyncTask 有一个内部标志,用于在任务被取消时设置。将 while (running) 替换为 while (!isCancelled())。在这种简单情况下,您不需要覆盖 onCancelled() 方法。参考链接:http://developer.android.com/reference/android/os/AsyncTask.html#isCancelled() - ToolmakerSteve

11

简单来说:不要使用 AsyncTaskAsyncTask 设计用于短时间内迅速结束的操作(数十秒),因此不需要取消操作。"音频文件播放" 并不符合这一条件。在普通音频文件播放中,甚至不需要后台线程。


34
不冒犯迈克,但那不是可以接受的答案。AsyncTask有一个cancel方法,应该有效。据我所知,它似乎并不起作用——但即使我使用错误的方法,也应该有一种正确的方法来取消任务。否则这个方法就不会存在。即使是短时间的任务也可能需要被取消——我有一个Activity,在加载时立即开始一个AsyncTask,如果用户在打开任务后立即返回,那么当任务完成但没有上下文可用于其onPostExecute时,他们将在一秒钟后看到一个Force Close。 - Eric Mill
10
@Klondike:我不知道谁是“Mike”,但这并不是一个可以接受的答案。你有权利持有自己的意见。AsyncTask有一个cancel方法,应该能够使用。在Java中取消线程已经成为问题约15年了,与Android没有太大关系。关于你提到的“强制关闭”情况,可以通过一个布尔变量来解决,在onPostExecute()中测试该变量以确定是否应该继续进行工作。 - CommonsWare
天啊,我真不敢相信我说了“Mike”,对此我深表歉意。在Java中取消线程可能已经是一个15年的问题,但如果它不可靠,那么在Android SDK中就不应该有一个布尔标记的方法来暗示线程将被主动中断,如果它是那么不确定性的话。这个方法干脆就不存在。 - Eric Mill
1
@Tejaswi Yerukalapudi:更多的是它不会自动执行任何操作。请参见此问题上的已接受答案。 - CommonsWare
10
在AsyncTask的doInBackground中,你应该定期检查isCancelled方法。文档中明确写着:http://developer.android.com/reference/android/os/AsyncTask.html#isCancelled()。请注意不要改变原意并尽量使翻译易于理解。 - Christopher Perry
显示剩余3条评论

4
这是我写AsyncTask的方法: 关键在于添加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;
    }

1
我发现在异步任务上简单地调用cancel(true)并定期检查isCancelled()确实可以工作,但是根据你的任务在做什么,可能需要长达60秒才能被中断。添加Thread.sleep(1)可以使其立即被中断。(异步任务进入等待状态而不是立即被丢弃)。谢谢。 - John J Smith

4

唯一的方法是通过检查isCancelled()方法的值,并在返回true时停止播放。


0

我们的全局AsyncTask类变量

LongOperation LongOperationOdeme = new LongOperation();

一个中断AsyncTask的KEYCODE_BACK操作

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

这对我来说可以运行。


0

我不喜欢不必要地强制中断我的异步任务,例如使用cancel(true),因为它们可能有需要释放的资源,例如关闭套接字或文件流、将数据写入本地数据库等。另一方面,我也遇到过异步任务拒绝在某些情况下结束的情况,例如当主活动正在关闭并且我从活动的onPause()方法内请求异步任务结束时。因此,这不仅仅是调用running = false的问题。我必须采取混合解决方案:既调用running = false,然后给异步任务几毫秒的时间来完成,然后再调用cancel(false)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;
}

作为一个副作用,doInBackground() 完成后,有时会调用 onCancelled() 方法,有时会调用 onPostExecute() 方法。但至少异步任务终止是有保障的。

看起来像是竞态条件。 - msangel

0
关于Yanchenko在2010年4月29日的回答: 当您的“doInBackground”代码需要在每次执行AsyncTask时执行多次时,使用“while(running)”方法很好。如果您的“doInBackground”代码只需要在每次执行AsyncTask时执行一次,则将所有代码都包装在“while(running)”循环中将无法阻止后台代码(后台线程)在取消AsyncTask本身时运行,因为“while(running)”条件仅在while循环内的所有代码至少执行一次后才会被评估。因此,您应该要么 (a.) 将您的“doInBackground”代码分解成多个“while(running)”块,要么 (b.) 在“Cancelling a task”下所述的“doInBackground”代码中执行多个“isCancelled”检查,如https://developer.android.com/reference/android/os/AsyncTask.html所述。
对于选项(a.),可以按以下方式修改Yanchenko的答案:
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;
}

// ...

对于选项 (b.),你在“doInBackground”中的代码会像这样:

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

// ...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接