AsyncTask在概念上确实存在缺陷吗?还是我只是想念一些东西?


264

我已经研究了这个问题好几个月了,并提出了不同的解决方案,我不满意,因为它们都是大型黑客。我仍然不敢相信,一个存在设计缺陷的类将其纳入了框架,并且没有人在谈论它,所以我想我肯定一定会丢失一些东西。

问题出在哪里AsyncTask。根据文档吧

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

然后,该示例继续说明如何showDialog()在中调用某些示例性方法onPostExecute()。但是,这似乎完全是想做的,因为显示对话框始终需要引用有效对象Context,而AsyncTask 绝不能持有对上下文对象的强引用

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

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

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

最重要的是,即使有了这种解决方法,您仍然面临这样的问题:当上下文被销毁时,您没有记录它触发的任务。这意味着在重新创建上下文时(例如,在屏幕方向更改后),您必须重新启动所有任务。这是缓慢且浪费的。

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

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

现在我只想知道:我只是大量丢失了东西还是AsyncTask确实完全有缺陷?您的经验如何处理?您是如何解决这些问题的?

感谢您的输入。


1
如果您感到好奇,我们最近在点火核心库中添加了一个名为IgnitedAsyncTask的类,该类使用下面的Dianne概述的连接/断开连接模式在所有回调中添加了对类型安全上下文访问的支持。它还允许抛出异常并在单独的回调中处理它们。见github.com/kaeppler/ignition-core/blob/master/src/com/github/…–
Matthias

看一下这个:gist.github.com/1393552
Matthias

1
这个问题也是相关的。
亚历克斯·洛克伍德

我将异步任务添加到arraylist中,并确保在特定点将其全部关闭。
NightSkyCode 2014年

Answers:


86

这样的事情怎么样:

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 2010年

13
onDestroy()方法将mActivity设置为null。谁拥有该活动的引用并不重要,因为该活动仍在运行。在调用onDestroy()之前,活动窗口将始终有效。通过在那里设置为null,异步任务将知道该活动不再有效。(而且当配置发生更改时,将调用上一个活动的onDestroy(),而下一个活动的onCreate()会在主循环之间不处理任何消息的情况下运行,因此AsyncTask将永远不会看到不一致的状态。)
hackbod 2010年

8
是的,但是它仍然不能解决我提到的最后一个问题:假设任务从互联网上下载了一些内容。使用这种方法,如果在任务运行时翻转屏幕3次,它将在每次屏幕旋转时重新启动,并且每个任务(最后一个任务除外)都会丢弃其结果,因为其活动参考为null。
Matthias 2010年

11
要在后台访问,您可能需要在mActivity周围进行适当的同步处理其为null时的运行时间,或者让后台线程仅使用Context.getApplicationContext()(它是应用程序的单个全局实例)。应用程序上下文在您可以执行的操作中受到限制(例如,没有像Dialog这样的UI),并且需要谨慎处理(如果不清除它们,将永久保留已注册的接收者和服务绑定),但是通常适用于不与特定组件的上下文无关。
hackbod 2010年

4
这是非常有用的,谢谢戴安娜!我希望文档一开始就一样好。
马提亚斯(Matthias)

20

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

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


静态引用要小心-即使对象有静态的强引用,我也已经看到对象被垃圾回收了。也许是Android类加载器的副作用,甚至是一个bug,但静态引用并不是跨活动生命周期交换状态的安全方法。但是,应用程序对象是我使用它的原因。
马提亚斯(Matthias)2010年

10
@Matthias:我没有说要使用静态引用。我说过要使用静态内部类。尽管两者的名称都具有“ static”,但存在很大的差异。
CommonsWare 2010年


5
我看到了-它们的关键是getLastNonConfigurationInstance(),而不是静态内部类。静态内部类不保留对其外部类的隐式引用,因此在语义上等效于普通的公共类。只是警告:活动中断(中断也可以是电话)时,不能保证onRetainNonConfigurationInstance()会被调用,因此您也必须将Task包裹在onSaveInstanceState()中,以确保真正可靠解。但是,好主意。
马提亚斯(Matthias)2010年

7
UM ... onRetainNonConfigurationInstance()始终在活动处于销毁和重新创建过程中被调用。在其他时间打电话没有任何意义。如果切换到另一个活动,则当前活动将被暂停/停止,但不会被破坏,因此异步任务可以继续运行并使用相同的活动实例。如果完成并说要显示对话框,则该对话框将正确显示为该活动的一部分,因此直到用户返回该活动后才向用户显示。您不能将AsyncTask放在捆绑包中。
hackbod 2010年

15

看起来AsyncTask是有点不仅仅是概念上有缺陷。兼容性问题也无法使用。Android文档显示为:

首次引入时,AsyncTasks在单个后台线程上串行执行。 从DONUT开始,它已更改为线程池,允许多个任务并行运行。 从HONEYCOMB开始,任务将返回到在单个线程上执行,以避免由并行执行引起的常见应用程序错误。 如果您确实希望并行执行,则可以 executeOnExecutor(Executor, Params...) 将该方法 版本与THREAD_POOL_EXECUTOR ; 一起使用但是,请参阅此处的注释以获取有关其使用的警告。

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

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

而且,如果您希望同时针对2.x和3.0+,那么这些东西将变得非常棘手。

另外,文档说:

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


12

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

一个Activity是一个Controller,并且Controller不应启动可能会使View失效的操作。也就是说,应从Model中使用AsyncTasks ,而从不绑定到Activity生命周期的类中使用AsyncTasks- 请记住,Activation在旋转时被破坏。(对于View,您通常不编程从android.widget.Button派生的类,但是可以。通常,您对View所做的唯一事情就是xml。)

换句话说,将AsyncTask派生类放置在Activity方法中是错误的。OTOH,如果我们一定不能在Activity中使用AsyncTasks,那么AsyncTask就会失去吸引力:它曾经被宣传为一种快速简便的修复方法。


5

我不确定是否确实会由于引用AsyncTask中的上下文而冒内存泄漏的风险。

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


2
是的-但是如果任务无限期地阻塞怎么办?任务旨在执行阻止操作,甚至可能永远不会终止。那里有内存泄漏。
马提亚斯

1
任何在无休止的循环中执行某些操作的工作人员,或仅在例如I / O操作上锁定的任何工作人员。
马提亚斯(Matthias)2010年

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

}

这类似于我们最初对Droid-Fu所做的操作。我们将保留对上下文对象的弱引用的映射,并在任务回调中进行查找以获得最新的引用(如果有)以对其执行回调。但是,我们的方法意味着只有一个实体维护此映射,而您的方法则没有,因此确实更好。
Matthias 2012年

1
您看过RoboSpice吗?github.com/octo-online/robospice。我相信这个系统会更好。
Snicolas 2012年

首页上的示例代码看起来像在泄漏上下文引用(内部类保留对外部类的隐式引用。)
Matthias 2012年

@Matthias,您是对的,这就是为什么我提议一个静态内部类,该类将在Activity上保留WeakReference。
Snicolas 2012年

1
@Matthias,我相信这已经成为话题。但是,加载程序并不像我们一样提供开箱即用的缓存,而且,加载程序往往比我们的lib更冗长。实际上,它们可以很好地处理游标,但是对于联网而言,基于缓存和服务的另一种方法更适合。参见neilgoodman.net/2011/12/26/…第1和第2部分
Snicolas 2012年

1

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


这取决于该任务在做什么。如果它只是加载/读取一些数据,那就可以了。但是,如果它更改了远程服务器上某些数据的状态,则我们希望使该任务能够运行到最后。
Vit Khudenko

@Arhimed,如果您按住UI线程,我会接受它onPause,因为与在其他任何地方按住它一样糟糕?即,您可以获得ANR?
杰夫·阿克塞尔罗德

究竟。我们无法阻止UI线程(无论是a onPause还是其他),因为我们有获得ANR的风险。
Vit Khudenko

1

您是完全正确的-这就是为什么在活动中不再使用异步任务/加载器来获取数据的原因越来越流行。一种新方法是使用Volley框架,该框架实质上在数据准备好后提供回调-与MVC模型更加一致。Volley在Google I / O 2013中得到了欢迎。不确定为什么更多的人不知道这一点。


谢谢。。。我要研究它。。。我不喜欢AsyncTask的原因是因为它使我受困于onPostExecute上的一组指令。我需要它。
carinlynchin

0

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


1
好吧,您强行关闭可能是因为我提到的问题:您尝试引用超出范围的上下文(即,其窗口已被破坏),这将导致框架异常。
马提亚斯(Matthias)2010年

不...实际上是因为队列很烂,内置在AsyncTask中。我总是使用getApplicationContext()。如果只有几个操作,我就不会遇到AsyncTask的问题...但是我正在编写一个媒体播放器,可以在后台更新专辑封面...在我的测试中,我有120张专辑没有专辑...因此我的应用程序并没有完全关闭,asynctask引发了错误……所以,我改为使用管理队列的队列构建了一个单例类,到目前为止,它工作得很好。
androidworkz


0

您最好将AsyncTask视为与Activity,Context,ContextWrapper等紧密结合的东西。充分了解其范围后,它会更加方便。

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

在离开Context时没有取消AsyncTask的情况下,您将遇到内存泄漏和NullPointerExceptions的情况,如果仅需要像Toast这样的简单对话框提供反馈,则单身应用上下文将有助于避免NPE问题。

AsyncTask并不都是坏事,但是肯定有很多不可思议的事情会导致一些无法预料的陷阱。


-1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.