265

我已经调查这个问题几个月了,想出了不同的解决方案,我不满意,因为它们都是大规模的黑客攻击。我仍然无法相信一个在设计上有缺陷的类进入了框架并且没有人在谈论它,所以我想我一定是遗漏了一些东西。

问题出在AsyncTask. 根据文件它

“允许在 UI 线程上执行后台操作并发布结果,而无需操作线程和/或处理程序。”

然后该示例继续显示如何showDialog()调用一些示例性方法onPostExecute()。然而,这对我来说似乎完全是人为的,因为显示对话框总是需要对有效的引用Context,并且 AsyncTask绝不能持有对上下文对象的强引用

原因很明显:如果触发任务的活动被破坏怎么办?这可能一直发生,例如因为您翻转了屏幕。如果任务持有对创建它的上下文的引用,那么您不仅会持有无用的上下文对象(窗口将被破坏,任何UI 交互都会因异常而失败!),您甚至可能会创建一个内存泄漏。

除非我的逻辑在这里有缺陷,否则这会转化为:onPostExecute()完全没用,因为如果您无法访问任何上下文,那么此方法在 UI 线程上运行有什么好处?你不能在这里做任何有意义的事情。

一种解决方法是不将上下文实例传递给 AsyncTask,而是传递一个Handler实例。这行得通:由于 Handler 松散地绑定了上下文和任务,因此您可以在它们之间交换消息而不会冒泄漏的风险(对吗?)。但这意味着 AsyncTask 的前提,即您不需要处理处理程序,是错误的。它也似乎在滥用 Handler,因为您在同一个线程上发送和接收消息(您在 UI 线程上创建它并在 onPostExecute() 中通过它发送,它也在 UI 线程上执行)。

最重要的是,即使使用这种解决方法,您仍然会遇到这样的问题,即当上下文被破坏时,您没有它触发的任务的记录。这意味着您必须在重新创建上下文时重新启动任何任务,例如在屏幕方向更改之后。这是缓慢而浪费的。

我对此的解决方案(在 Droid-Fu 库中实现)是维护WeakReferences 从组件名称到它们在唯一应用程序对象上的当前实例的映射。每当启动 AsyncTask 时,它都会在该映射中记录调用上下文,并且在每次回调时,它将从该映射中获取当前上下文实例。这确保您永远不会引用过时的上下文实例,并且您始终可以访问回调中的有效上下文,以便您可以在那里进行有意义的 UI 工作。它也不会泄漏,因为引用很弱,并且当给定组件的实例不再存在时会被清除。

尽管如此,它仍然是一个复杂的解决方法,并且需要对一些 Droid-Fu 库类进行子类化,这使得这是一种非常侵入性的方法。

现在我只想知道:我只是大量遗漏了某些东西还是 AsyncTask 真的完全有缺陷?您使用它的经验如何?你是如何解决这些问题的?

感谢您的输入。

4

12 回答 12

87

像这样的东西怎么样:

class MyActivity extends Activity {
    Worker mWorker;

    static class Worker extends AsyncTask<URL, Integer, Long> {
        MyActivity mActivity;

        Worker(MyActivity activity) {
            mActivity = activity;
        }

        @Override
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
            }
            return totalSize;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mActivity != null) {
                mActivity.setProgressPercent(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(Long result) {
            if (mActivity != null) {
                mActivity.showDialog("Downloaded " + result + " bytes");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mWorker = (Worker)getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = this;
        }

        ...
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new Worker(this);
        mWorker.execute(...);
    }
}
于 2010-07-29T02:10:40.363 回答
20

原因很明显:如果触发任务的活动被破坏怎么办?

手动解除活动与AsyncTaskin的关联onDestroy()。手动将新活动重新关联到AsyncTaskin onCreate()。这需要一个静态内部类或一个标准 Java 类,再加上大概 10 行代码。

于 2010-07-29T00:59:41.940 回答
15

它看起来AsyncTask不仅仅是概念上缺陷。它也因兼容性问题而无法使用。Android 文档中写道:

首次引入时,AsyncTask 在单个后台线程上串行执行。 从 DONUT 开始,这被更改为允许多个任务并行运行的线程池。 从 HONEYCOMB 开始,任务恢复到在单个线程上执行,以避免并行执行导致的常见应用程序错误。 如果你真的想要并行执行,你可以使用 executeOnExecutor(Executor, Params...) 这个方法的版本 THREAD_POOL_EXECUTOR; 但是,请参阅那里的评论以获取有关其使用的警告。

两者executeOnExecutor()在 API 级别 11(Android 3.0.x、HONEYCOMB)THREAD_POOL_EXECUTOR中添加。

这意味着如果您创建两个AsyncTasks 来下载两个文件,则在第一个完成之前不会开始第二次下载。如果您通过两台服务器聊天,而第一台服务器已关闭,则在与第一台服务器的连接超时之前,您将无法连接到第二台服务器。(当然,除非您使用新的 API11 功能,但这会使您的代码与 2.x 不兼容)。

而且,如果您想同时针对 2.x 和 3.0+,这些东西就变得非常棘手。

此外,文档说:

注意:使用工作线程时您可能会遇到的另一个问题是由于运行时配置更改(例如当用户更改屏幕方向时)导致您的活动意外重启,这可能会破坏您的工作线程。要了解如何在这些重启之一期间保留您的任务以及如何在活动被销毁时正确取消任务,请参阅 Shelves 示例应用程序的源代码。

于 2013-01-30T11:05:41.397 回答
12

AsyncTaskMVC的角度来看,可能我们所有人,包括 Google,都在滥用。

一个 Activity 是一个Controller,并且控制器不应该启动可能比View寿命更长的操作。也就是说, AsyncTasks 应该从Model使用,从一个未绑定到 Activity 生命周期的类中使用——请记住,Activity 在轮换时被销毁。(至于View,您通常不会编写从例如 android.widget.Button 派生的类,但您可以。通常,您对View所做的唯一事情就是 xml。)

换句话说,将 AsyncTask 派生类放在Activities 的方法中是错误的。OTOH,如果我们不能在活动中使用 AsyncTasks,AsyncTask 就会失去它的吸引力:它曾经被宣传为一种快速简便的解决方案。

于 2013-01-30T11:54:47.163 回答
5

我不确定您是否会因引用来自 AsyncTask 的上下文而冒内存泄漏的风险。

实现它们的常用方法是在 Activity 方法之一的范围内创建一个新的 AsyncTask 实例。因此,如果 Activity 被销毁,那么一旦 AsyncTask 完成,它就不会无法访问然后有资格进行垃圾收集吗?因此,对活动的引用无关紧要,因为 AsyncTask 本身不会挂起。

于 2010-07-28T23:52:57.137 回答
2

在您的活动上保留一个 WeekReference 会更可靠:

public class WeakReferenceAsyncTaskTestActivity extends Activity {
    private static final int MAX_COUNT = 100;

    private ProgressBar progressBar;

    private AsyncTaskCounter mWorker;

    @SuppressWarnings("deprecation")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task_test);

        mWorker = (AsyncTaskCounter) getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(this);
        }

        progressBar = (ProgressBar) findViewById(R.id.progressBar1);
        progressBar.setMax(MAX_COUNT);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_async_task_test, menu);
        return true;
    }

    public void onStartButtonClick(View v) {
        startWork();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new AsyncTaskCounter(this);
        mWorker.execute();
    }

    static class AsyncTaskCounter extends AsyncTask<Void, Integer, Void> {
        WeakReference<WeakReferenceAsyncTaskTestActivity> mActivity;

        AsyncTaskCounter(WeakReferenceAsyncTaskTestActivity activity) {
            mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(activity);
        }

        private static final int SLEEP_TIME = 200;

        @Override
        protected Void doInBackground(Void... params) {
            for (int i = 0; i < MAX_COUNT; i++) {
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(getClass().getSimpleName(), "Progress value is " + i);
                Log.d(getClass().getSimpleName(), "getActivity is " + mActivity);
                Log.d(getClass().getSimpleName(), "this is " + this);

                publishProgress(i);
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            if (mActivity != null) {
                mActivity.get().progressBar.setProgress(values[0]);
            }
        }
    }

}
于 2012-09-20T22:56:33.957 回答
1

为什么不直接覆盖拥有的 Activity 中的方法并从那里onPause()取消呢?AsyncTask

于 2012-08-08T05:23:27.770 回答
1

你是绝对正确的——这就是为什么在活动中不再使用异步任务/加载器来获取数据的趋势正在获得动力。其中一种新方法是使用Volley框架,该框架本质上在数据准备好后提供回调 - 与 MVC 模型更加一致。Volley 在 2013 年的 Google I/O 上流行起来。不知道为什么更多的人没有意识到这一点。

于 2014-07-15T14:25:15.170 回答
0

就个人而言,我只是扩展 Thread 并使用回调接口来更新 UI。如果没有 FC 问题,我永远无法让 AsyncTask 正常工作。我还使用非阻塞队列来管理执行池。

于 2010-07-29T00:03:22.917 回答
0

我认为取消工作,但它没有。

他们在这里 RTFMing 关于它:

""如果任务已经开始,那么 mayInterruptIfRunning 参数确定是否应该中断执行该任务的线程以尝试停止该任务。"

然而,这并不意味着线程是可中断的。这是 Java 的事情,而不是 AsyncTask 的事情。”

http://groups.google.com/group/android-developers/browse_thread/thread/dcadb1bc7705f1bb/add136eb4949359d?show_docid=add136eb4949359d

于 2010-12-14T01:21:25.137 回答
0

您最好将 AsyncTask 视为与 Activity、Context、ContextWrapper 等更紧密耦合的东西。当它的范围被完全理解时,它会更方便。

确保您在生命周期中有一个取消政策,以便最终将其作为垃圾收集并且不再保留对您的活动的引用,并且它也可以被垃圾收集。

Without canceling your AsyncTask while traversing away from your Context you will run into memory leaks and NullPointerExceptions, if you simply need to provide feedback like a Toast a simple dialog then a singleton of your Application Context would help avoid the NPE issue.

AsyncTask isn't all bad but there's definitely a lot of magic going on that can lead to some unforeseen pitfalls.

于 2014-09-07T17:29:49.480 回答
-1

至于“使用它的经验”:可以将进程连同所有 AsyncTasks 一起杀死,Android 将重新创建活动堆栈,以便用户不会提及任何内容。

于 2013-01-30T05:16:39.570 回答