AsyncTask 真的在概念上存在缺陷还是我只是遗漏了啥?

Posted

技术标签:

【中文标题】AsyncTask 真的在概念上存在缺陷还是我只是遗漏了啥?【英文标题】:Is AsyncTask really conceptually flawed or am I just missing something?AsyncTask 真的在概念上存在缺陷还是我只是遗漏了什么? 【发布时间】:2011-03-22 09:52:21 【问题描述】:

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

问题在于AsyncTask。根据文档它

"允许执行背景 操作并发布结果 无需操作的 UI 线程 线程和/或处理程序。”

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

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

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

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

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

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

不过,这是一个复杂的解决方法,并且需要对一些 Droid-Fu 库类进行子类化,这使得这是一种相当侵入性的方法。

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

感谢您的意见。

【问题讨论】:

如果您好奇,我们最近在点火核心库中添加了一个名为 IgnitedAsyncTask 的类,它使用 Dianne 概述的连接/断开模式在所有回调中添加了对类型安全上下文访问的支持以下。它还允许抛出异常并在单独的回调中处理它们。见github.com/kaeppler/ignition-core/blob/master/src/com/github/… 看看这个:gist.github.com/1393552 这个question也是相关的。 我将异步任务添加到一个数组列表中,并确保在某个时间点将它们全部关闭。 【参考方案1】:

这样的事情怎么样:

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(...);
    

【讨论】:

是的,mActivity 将为 != null,但如果没有对您的 Worker 实例的引用,那么该实例的任何引用也将受到垃圾清除的影响。如果您的任务确实永远运行,那么您无论如何都会发生内存泄漏(您的任务) - 更不用说您正在耗尽手机电池。此外,如其他地方所述,您可以在 onDestroy 中将 mActivity 设置为 null。 onDestroy() 方法将 mActivity 设置为 null。在此之前谁拥有对活动的引用并不重要,因为它仍在运行。在调用 onDestroy() 之前,活动的窗口将始终有效。通过在此处设置为 null,异步任务将知道该活动不再有效。 (并且当配置更改时,前一个活动的 onDestroy() 会被调用,而下一个活动的 onCreate() 会在它们之间处理的主循环上没有任何消息的情况下运行,因此 AsyncTask 永远不会看到不一致的状态。) 是的,但它仍然没有解决我提到的最后一个问题:想象一下这个任务从互联网上下载了一些东西。使用这种方法,如果您在任务运行时翻转屏幕 3 次,它将在每次屏幕旋转时重新启动,并且除了最后一个任务之外的每个任务都将其结果丢弃,因为它的活动引用为空。 要在后台访问,您需要在 mActivity 周围进行适当的同步处理它为 null 时的运行时间,或者让后台线程只使用Context.getApplicationContext() 是应用程序的单个全局实例。应用程序上下文受限于您可以执行的操作(例如,没有像 Dialog 这样的 UI)并且需要小心谨慎(如果您不清理它们,注册的接收器和服务绑定将永远保留),但通常适用于以下代码'不绑定到特定组件的上下文。 这非常有帮助,谢谢 Dianne!我希望文档一开始也一样好。【参考方案2】:

原因很明显:如果 活动被破坏 触发了任务?

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

【讨论】:

小心静态引用——我已经看到对象被垃圾回收,即使它们有静态强引用。也许是 android 类加载器的副作用,甚至是一个错误,但静态引用并不是在 Activity 生命周期中交换状态的安全方式。然而,app 对象是我使用它的原因。 @Matthias:我没有说要使用静态引用。我说使用静态内部类。尽管名称中都包含“静态”,但两者之间存在很大差异。 @Pentium10: github.com/commonsguy/cw-android/tree/master/Rotation/… 我明白了——这里的关键是 getLastNonConfigurationInstance(),而不是静态内部类。静态内部类不保留对其外部类的隐式引用,因此它在语义上等同于普通的公共类。只是一个警告:onRetainNonConfigurationInstance() 不能保证在活动被中断时被调用(中断也可以是电话),所以你也必须将你的任务打包在 onSaveInstanceState() 中,以获得真正可靠的解决方案。但是,好主意。 Um... onRetainNonConfigurationInstance() 总是在 Activity 正在被销毁和重新创建的过程中调用。在其他时间打电话是没有意义的。如果切换到另一个活动,当前活动会暂停/停止,但不会被销毁,因此异步任务可以继续运行并使用相同的活动实例。如果它完成并说显示一个对话框,该对话框将作为该活动的一部分正确显示,因此在用户返回活动之前不会向用户显示。您不能将 AsyncTask 放在 Bundle 中。【参考方案3】:

看起来AsyncTask 有点更多而不仅仅是概念上有缺陷。它也因兼容性问题而无法使用。 Android 文档如下:

首次引入时,AsyncTasks 是在单个后台线程上串行执行的。 从 DONUT 开始,它被更改为允许多个任务并行运行的线程池。 启动HONEYCOMB后,任务又回到单线程执行,避免并行执行引起的常见应用错误。 如果你真的想要并行执行,可以使用 @ 987654323@ 此方法的版本 THREAD_POOL_EXECUTOR; 但是,请参阅那里的评论以获取有关其使用的警告。

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

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

如果你想同时针对 2.x 和 3.0+,那么事情就变得非常棘手了。

另外,docs 说:

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

【讨论】:

【参考方案4】:

MVC 的角度来看,可能我们所有人(包括 Google)都在滥用 AsyncTask

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

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

【讨论】:

【参考方案5】:

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

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

【讨论】:

true -- 但如果任务无限期阻塞怎么办?任务旨在执行阻塞操作,甚至可能是永远不会终止的操作。你有你的内存泄漏。 任何在无限循环中执行某事的工作人员,或任何刚刚锁定的事情,例如在 I/O 操作上。【参考方案6】:

在您的活动中保留一个 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 所做的。我们将保留对上下文对象的弱引用映射,并在任务回调中进行查找以获得最新的引用(如果可用)来运行回调。然而,我们的方法意味着只有一个实体来维护这个映射,而您的方法没有,所以这确实更好。 你看过 RoboSpice 吗? github.com/octo-online/robospice。我确实相信这个系统会更好。 首页的示例代码看起来像是泄露了上下文引用(内部类保持对外部类的隐式引用。)不服!! @Matthias,你是对的,这就是为什么我提出了一个静态内部类,它将在 Activity 上保存一个 WeakReference。 @Matthias,我相信这开始离题了。但是加载器不像我们那样提供开箱即用的缓存,而且加载器往往比我们的库更冗长。实际上,它们可以很好地处理游标,但对于网络,基于缓存和服务的不同方法更适合。请参阅neilgoodman.net/2011/12/26/… 第 1 部分和第 2 部分【参考方案7】:

为什么不直接覆盖所属 Activity 中的 onPause() 方法并从那里取消 AsyncTask

【讨论】:

这取决于该任务正在做什么。如果它只是加载/读取一些数据,那就没问题了。但如果它改变了远程服务器上某些数据的状态,那么我们更愿意让任务能够运行到最后。 @Arhimed 如果你在onPause 中阻止 UI 线程,我认为它与在其他任何地方阻止它一样糟糕?即,您可以获得 ANR? 完全正确。我们不能阻止 UI 线程(无论是 onPause 还是其他任何东西),因为我们有可能获得 ANR。【参考方案8】:

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

【讨论】:

谢谢...我会调查一下...我不喜欢 AsyncTask 的原因是因为它让我坚持使用 PostExecute 的一组指令...除非我破解它比如每次需要时使用接口或覆盖它。【参考方案9】:

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

【讨论】:

好吧,您的强制关闭可能是因为我提到的问题:您尝试引用超出范围的上下文(即其窗口已被破坏),这将导致框架异常。 不...实际上是因为队列很烂,内置在 AsyncTask 中。我总是使用 getApplicationContext()。如果只是几个操作,我对 AsyncTask 没有问题......但我正在编写一个媒体播放器,在后台更新专辑封面......在我的测试中,我有 120 张没有艺术的专辑......所以,虽然我的应用程序并没有完全关闭,asynctask 正在抛出错误......所以我构建了一个带有管理进程的队列的单例类,到目前为止它运行良好。【参考方案10】:

我认为取消有效,但它没有。

他们在这里 RTFMing:

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

但这并不意味着线程是可中断的。那是一个 Java 的东西,而不是 AsyncTask 的东西。”

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

【讨论】:

【参考方案11】:

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

确保您在生命周期中有取消政策,这样它最终会被垃圾回收,不再保留对您的活动的引用,它也可以被垃圾回收。

在离开 Context 时如果不取消 AsyncTask,您将遇到内存泄漏和 NullPointerExceptions,如果您只需要像 Toast 一个简单的对话框一样提供反馈,那么您的 Application Context 的单例将有助于避免 NPE 问题。

AsyncTask 并不全是坏事,但肯定有很多魔法正在发生,可能会导致一些无法预料的陷阱。

【讨论】:

【参考方案12】:

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

【讨论】:

以上是关于AsyncTask 真的在概念上存在缺陷还是我只是遗漏了啥?的主要内容,如果未能解决你的问题,请参考以下文章

电脑有漏洞,修还是不修,听别人说不要修,会使电脑运行速度变慢,是真的吗

你真的了解分层架构吗?

onPreExecute() 和 onPostExecute() 是在 UI 线程还是在启动 AsyncTask 的线程上执行?

AsyncTask 线程永不消亡

Android 我应该使用 AsyncTask 还是 IntentService 进行 REST API 调用? [复制]

Android - 执行 AsyncTask 线程的问题