AsyncTask真的在概念上存在缺陷吗,还是我漏掉了什么?

265
我已经调查了这个问题数月,提出了不同的解决方案,但都不满意,因为它们都是巨大的hack。我仍然无法相信在框架中存在这样一个设计缺陷的类,并且没有人谈论它,所以我想我一定是漏掉了什么。
问题出在AsyncTask上。根据文档,它允许在不必操作线程和/或处理程序的情况下,在后台执行操作并在UI线程上发布结果。例子继续展示了如何在onPostExecute()中调用一些showDialog()方法,然而,这对我来说似乎完全是牵强附会的,因为显示对话框总是需要引用有效的上下文,而AsyncTask绝不能保留对上下文对象的强引用。
原因很明显:如果触发任务的活动被销毁怎么办?这可能随时发生,例如,因为您翻转了屏幕。如果任务保留了创建它的上下文的引用,那么你不仅保存了一个无用的上下文对象(窗口将已被销毁,并且任何UI交互都会失败并抛出异常!),而且还有可能造成内存泄漏。
除非我的逻辑出现错误,否则这意味着:onPostExecute()是完全无用的,因为如果您没有访问任何上下文,这个方法在UI线程上运行有什么好处呢?你在这里无法做任何有意义的事情。
一种解决方法是不向AsyncTask传递上下文实例,而是传递Handler实例。这样可以避免泄漏的风险,因为Handler松散地绑定了上下文和任务,你可以在它们之间交换消息(对吗?)。但这意味着AsyncTask的前提是错误的,即你不需要烦恼处理程序。同时,这似乎滥用了Handler,因为你在同一个线程上发送和接收消息(你在UI线程上创建它,并在onPostExecute()中通过它发送)。
更糟糕的是,即使使用这种解决方法,当上下文被销毁时,你仍然无法记录所触发的任务。这意味着在重新创建上下文后(例如,在屏幕方向改变后),你必须重新启动所有的任务。这很慢也很浪费。
我的解决方案(在Droid-Fu库中实现)是在唯一的应用程序对象上维护从组件名称到它们当前实例的WeakReference映射。每当启动一个AsyncTask时,它就会在该映射中记录调用上下文,并在每个回调时从该映射中获取当前上下文实例。这确保你永远不会引用过期的上下文实例,同时在回调中始终有一个有效的上下文,因此你可以在那里进行有意义的UI工作。这也不会泄漏,因为引用是弱引用,并且在给定组件的任何实例不存在时被清除。
尽管如此,这仍然是一种复杂的解决方法,并需要对Droid-Fu库中的某些类进行子类化,使其成为相当侵入性的方法。

现在我只是想知道:我是不是漏掉了什么重要的东西,还是AsyncTask真的存在很大的缺陷?你们使用它的经验如何?你们是如何解决这些问题的?

谢谢你的回答。


2
如果您感到好奇,我们最近在点火核心库中添加了一个名为IgnitedAsyncTask的类,它通过使用Dianne下面概述的连接/断开模式,在所有回调中添加了对类型安全上下文访问的支持。它还允许抛出异常并在单独的回调中处理它们。请参见https://github.com/kaeppler/ignition-core/blob/master/src/com/github/ignition/core/tasks/IgnitedAsyncTask.java。 - mxk
请查看此链接:https://gist.github.com/1393552 - mxk
1
这个问题也与此问题有关。 - Alex Lockwood
我将异步任务添加到一个ArrayList中,并确保在某个特定点关闭它们。 - NightSkyCode
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(...);
    }
}

5
是的,mActivity将不等于null,但是如果没有引用到您的Worker实例,则该实例的任何引用也会受到垃圾回收的影响。如果您的任务确实无休止地运行,那么您就会有一个内存泄漏问题(您的任务)-更不用说你耗尽了手机电池了。此外,正如其他地方提到的那样,您可以在onDestroy中将mActivity设置为null。 - EboMike
13
onDestroy()方法将mActivity设置为null。在此之前,无论谁持有对该Activity的引用,它仍然在运行。直到onDestroy()被调用,该活动的窗口将始终有效。通过在此处设置为null,异步任务将知道该活动不再有效。 (当配置更改时,将调用前一个Activity的onDestroy()方法,并运行下一个Activity的onCreate()方法,它们之间不会处理任何主循环消息,因此AsyncTask将永远不会看到不一致的状态。) - hackbod
8
没错,但是这仍然无法解决我提到的最后一个问题:想象一下任务从互联网上下载某些东西。使用这种方法,如果在任务运行时翻转屏幕3次,则每次屏幕旋转都会重新启动任务,并且除了最后一个任务外,每个任务都会因其活动引用为空而丢弃其结果。 - mxk
4
你的代码并不依赖于它被调用,也就是说,如果应用程序完全重新启动(进程关闭),你会在没有异步任务的情况下启动。你将其用作一种优化方式,以保持相同的异步任务在活动实例之间运行。 - hackbod
11
要在后台访问,您需要在mActivity周围放置适当的同步并处理其为空的情况,或者让后台线程只获取Context.getApplicationContext(),这是应用程序的单个全局实例。应用程序上下文受到限制(例如没有UI对话框),需要一些小心处理(如果不清理,注册的接收器和服务绑定将永远保留),但通常适用于不与特定组件的上下文相关的代码。 - hackbod
显示剩余12条评论

20

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

onDestroy() 中手动取消与 AsyncTask 的关联,onCreate() 中手动重新将新活动与 AsyncTask 关联。这需要一个静态内部类或标准 Java 类,加上大约 10 行代码。


小心使用静态引用——我曾经看到过即使有静态强引用,对象也被垃圾回收的情况。这可能是Android类加载器的副作用,或者是一个错误,但静态引用不是跨活动生命周期交换状态的安全方式。然而,应用程序对象是安全的,这就是为什么我使用它的原因。 - mxk
10
@Matthias: 我并没有说要使用静态引用,我是建议使用一个静态内部类。虽然两者名称中都带有“static”,但它们之间存在着实质性的区别。 - CommonsWare
3
@Pentium10:http://github.com/commonsguy/cw-android/tree/master/Rotation/RotationAsync/ - CommonsWare
5
我明白了 - 这里关键是getLastNonConfigurationInstance(),而不是静态内部类。静态内部类不会对其外部类保留任何隐式引用,因此从语义上讲,它与普通的public类等价。只是一个警告:当活动被中断时(中断也可能是电话),onRetainNonConfigurationInstance()不能保证被调用,因此您还需要在onSaveInstanceState()中打包您的Task,以获得真正稳定的解决方案。但还是个好主意。 - mxk
7
Um... onRetainNonConfigurationInstance()方法总是在Activity即将被销毁并重新创建时调用,其他时间调用该方法没有意义。如果切换到另一个Activity,当前Activity会被暂停/停止,但不会被销毁,所以异步任务可以继续运行并使用同一Activity实例。如果异步任务完成并显示对话框,对话框将正确地显示为该Activity的一部分,并且直到用户返回该Activity之前,对话框才会显示给用户。你不能将AsyncTask放入Bundle中。 - hackbod
显示剩余3条评论

15
看起来AsyncTask不仅在概念上存在缺陷,而且由于兼容性问题而无法使用。Android文档中写道:

初次引入时,AsyncTasks在单个后台线程上串行执行。从DONUT开始,它被更改为线程池,允许多个任务并行操作。从HONEYCOMB开始,任务再次在单个线程上执行,以避免由并行执行引起的常见应用程序错误。如果您真正想要并行执行,则可以使用此方法的executeOnExecutor(Executor, Params...)版本,并使用THREAD_POOL_EXECUTOR;但是,请查看其中的评论以获取有关其使用警告的信息。

executeOnExecutor()THREAD_POOL_EXECUTOR都是在API级别11中添加的(Android 3.0.x,HONEYCOMB)。 这意味着,如果您创建两个AsyncTask下载两个文件,则第二个下载将在第一个完成之前不会开始。 如果您通过两个服务器进行聊天,并且第一个服务器关闭,则在第一个连接超时之前,您将无法连接到第二个服务器。(当然,除非您使用新的API11功能,但这将使您的代码与2.x不兼容)。

而且,文档还说:

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


12

可能我们所有人,包括Google在内,都在MVC(模型视图控制器)的角度上误用了AsyncTask

一个活动是一个控制器,并且控制器不应该启动可能超出视图生命周期的操作。也就是说,AsyncTasks应该从模型中使用,从一个不绑定到Activity生命周期的类中使用--请记住,Activities在旋转时会被销毁。(至于视图,通常情况下你不需要编写派生自例如android.widget.Button之类的类,但是你可以这样做。通常,你只需要关注xml文件即可。)

换句话说,将AsyncTask派生类放置在Activities方法中是错误的。另一方面,如果我们不能在Activities中使用AsyncTasks,AsyncTask就失去了其吸引力:它曾被宣传为一个快速而简单的解决方案。


5

我不确定一个AsyncTask对上下文的引用是否会导致内存泄漏。

通常的实现方式是在Activity的方法范围内创建一个新的AsyncTask实例。因此,如果Activity被销毁,那么一旦AsyncTask完成,它就变得不可达,然后可以进行垃圾回收。所以对Activity的引用并不重要,因为AsyncTask本身不会停留。


2
真实的问题是,如果任务无限期地阻塞了怎么办?任务旨在执行阻塞操作,甚至可能是永远不会终止的操作。这就是你的内存泄漏问题所在。 - mxk
1
任何在无限循环中执行某些操作的工作线程,或者在 I/O 操作上仅仅锁死的东西。 - mxk

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

}

1
你看过RoboSpice了吗?https://github.com/octo-online/robospice。我相信这个系统甚至更好。 - Snicolas
@Matthias,你是在说RoboSpice的首页吗?哪里有泄漏的问题吗? - Snicolas
谢谢@Matthias,但你真的应该看一下API。实际上,如果活动被销毁(确实是在onPause期间),这个内部类的实例不会创建任何内存泄漏,因为它将被垃圾回收。我们的框架处理了所有这些问题,您可以安全地使用内部类,在请求返回结果时更方便地修改您的活动。我们在框架中真正处理了所有这些问题。 - Snicolas
哦,所以当活动被销毁时,侦听器也被销毁了?我明白了,我的错 - 我曾经认为侦听器是在活动重新启动后仍然存在的那个部分。 - mxk
1
@Matthias,我认为这开始偏离主题了。但是,与我们的库不同,加载器不会提供缓存功能,而且加载器往往比我们的库更冗长。实际上,它们对于游标处理得非常好,但对于网络,基于缓存和服务的不同方法更加适合。请参见http://neilgoodman.net/2011/12/26/modern-techniques-for-implementing-rest-clients-on-android-4-0-and-below-part-1/第1部分和第2部分。 - Snicolas
显示剩余6条评论

1

你说得很对 - 这就是为什么越来越多的人开始避免在活动中使用异步任务/加载器来获取数据。其中一种新的方法是使用Volley框架,它基本上会在数据准备好后提供回调 - 这更符合MVC模型的一致性。Volley在Google I/O 2013中变得流行起来。不确定为什么更多的人没有意识到这一点。


谢谢你的建议,我会研究一下。我不喜欢AsyncTask的原因是它让我只能使用一个固定的onPostExecute指令,除非我像使用接口或每次需要时覆盖它那样去修改它。 - carinlynchin

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

这取决于任务正在做什么。如果它只是加载/读取一些数据,那么就可以了。但是,如果它更改了远程服务器上某些数据的状态,那么我们更愿意让任务具有运行到结束的能力。 - Vit Khudenko
@Arhimed 我们认为,如果您在 onPause 中阻塞 UI 线程,那么它和在其他任何地方阻塞一样糟糕,也就是说,您可能会遇到 ANR 错误? - Jeff Axelrod
准确地说,我们不能阻塞UI线程(无论是onPause还是其他任何东西),因为这样会有ANR的风险。 - Vit Khudenko

0

你最好将AsyncTask视为与Activity、Context、ContextWrapper等更紧密耦合的东西。只有完全理解其范围时,它才更方便。

确保在生命周期中设置取消策略,以便最终进行垃圾回收,并且不再保留对您的活动的引用,从而可以进行垃圾回收。

如果在离开上下文时不取消AsyncTask,则会遇到内存泄漏和NullPointerException问题。如果您只需要提供像Toast或简单对话框之类的反馈,则应用程序上下文的单例将有助于避免NPE问题。

AsyncTask并非一无是处,但其中确实存在许多神奇的事情,可能会导致一些意想不到的问题。


0

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