AsyncTask在概念上是否真的有缺陷,或者我只是缺少某些东西?

Mat*_*ias 260 concurrency android handler android-asynctask

我已经调查了这个问题好几个月了,想出了不同的解决方案,我不满意,因为它们都是大规模的黑客攻击.我仍然无法相信一个设计有缺陷的课程已经进入框架而没有人在讨论它,所以我想我一定要错过一些东西.

问题在于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是否真的完全有缺陷?您的使用经验如何?你是怎么解决这些问题的?

感谢您的输入.

hac*_*bod 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(...);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • onDestroy()方法将mActivity设置为null.在此之前谁拥有对活动的引用并不重要,因为它仍然在运行.在调用onDestroy()之前,活动的窗口始终有效.通过在那里设置为null,异步任务将知道该活动不再有效.(当配置发生变化时,前一个活动的onDestroy()被调用,下一个onCreate()运行时,主循环之间没有任何消息,因此AsyncTask永远不会看到不一致的状态.) (13认同)
  • 要在后台访问,您需要在mActivity*之间进行适当的同步,并且*处理在null时运行的时间,或者让后台线程只接受Context.getApplicationContext(),这是一个单一的全局实例.应用程序.应用程序上下文受限于您可以执行的操作(例如没有像Dialog这样的UI)并且需要一些小心(如果您不清理它们,注册的接收器和服务绑定将永远保留),但通常适用于不是与特定组件的上下文联系在一起. (11认同)
  • 是的,但它仍然没有解决我提到的最后一个问题:想象任务下载互联网上的东西.使用这种方法,如果在任务运行时翻转屏幕3次,它将在每次屏幕旋转时重新启动,除了最后一个任务之外的每个任务都会抛出其结果,因为它的活动引用为空. (8认同)
  • 是的,mActivity将是!= null,但是如果没有对您的Worker实例的引用,那么该实例的任何引用也将被删除.如果您的任务确实运行,那么无论如何都会发生内存泄漏(您的任务) - 更不用说您正在耗尽手机电池.此外,如其他地方所述,您可以在onDestroy中将mActivity设置为null. (5认同)
  • 这非常有帮助,谢谢Dianne!我希望文档首先是好的. (4认同)
  • 不,它不会重新启动.当前的AsyncTask通过onRetainNonConfigurationInstance()传播到新的Activity实例. (3认同)
  • 您的代码不依赖于它被调用 - 也就是说,如果应用程序完全重新启动(进程消失),您将在没有异步任务的情况下启动.您正在使用它作为优化来保持在活动实例之间运行相同的异步任务. (3认同)
  • 此外,如果您的任务没有终止,这正是您创建内存泄漏的方式. (2认同)
  • 还有一个问题:onReatainNonConfigurationInstance()的API文档读取"这个函数纯粹被称为优化,你不能依赖它被调用." 但是你的代码依赖于它被调用.那是问题吗?在哪种情况下不会调用该方法? (2认同)
  • 我明白了,谢谢.如果我需要访问doInBackground()中的上下文怎么办?那么我是否需要将活动引用声明为volatile,因为doInBackground()将在工作线程中调用? (2认同)

Com*_*are 20

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

手动取消活动与AsyncTaskin onDestroy().手动将新活动重新关联到AsyncTaskin onCreate().这需要静态内部类或标准Java类,加上可能有10行代码.

  • @Matthias:我没说过使用静态引用.我说要使用静态内部类.虽然他们的名字中都有"静态",但存在很大差异. (10认同)
  • 当活动处于被销毁和重新创建的过程中时,总会调用on_etRetainNonConfigurationInstance().在其他时间打电话是没有意义的.如果发生切换到另一个活动,则暂停/停止当前活动,但不会销毁它,因此异步任务可以继续运行并使用相同的活动实例.如果它完成并显示对话框,则对话框将正确显示为该活动的一部分,因此在用户返回活动之前不会向用户显示.你不能把AsyncTask放在一个Bundle中. (7认同)
  • 我明白了 - 这里的关键是getLastNonConfigurationInstance(),而不是静态内部类.静态内部类不保留对其外部类的隐式引用,因此它在语义上等同于普通的公共类.只是一个警告:onRetainNonConfigurationInstance()不保证在活动中断时被调用(中断也可以是电话),所以你必须在onSaveInstanceState()中包含你的任务,以获得真正的实体解.但是,好主意. (5认同)
  • @ Pentium10:http://github.com/commonsguy/cw-android/tree/master/Rotation/RotationAsync/ (3认同)

184*_*615 15

看起来AsyncTask是有点不仅仅是概念上的缺陷.兼容性问题也无法使用它.Android文档阅读:

首次引入时,AsyncTasks在单个后台线程上串行执行. 从DONUT开始,这被改为一个线程池,允许多个任务并行运行. 启动HONEYCOMB,任务将恢复在单个线程上执行,以避免由并行执行引起的常见应用程序错误. 如果您真的想要并行执行,可以使用 executeOnExecutor(Executor, Params...) 此方法 版本THREAD_POOL_EXECUTOR ; 但是,请在那里查看有关其使用的警告.

双方executeOnExecutor()THREAD_POOL_EXECUTOR在API级别11(的Android 3.0.x的,蜂巢).

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

如果你想要同时针对2.x和3.0+,这些东西变得非常棘手.

此外,文档说:

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


184*_*615 12

可能我们所有人,包括谷歌,都AsyncTaskMVC的角度来滥用.

Activity是一个Controller,控制器不应该启动可能超过View的操作.也就是说,AsyncTasks应该从Model中使用,来自未绑定到Activity生命周期的类 - 请记住,活动在轮换时被销毁.(对于View,你通常不会编译派生自android.widget.Button的类,但你可以.通常,你对View做的唯一一件事就是xml.)

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


oli*_*oli 5

我不确定通过引用AsyncTask中的上下文来冒险导致内存泄漏.

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

  • 是的 - 但如果任务无限期地阻止怎么办?任务旨在执行阻塞操作,甚至可能是永不终止的操作.你有内存泄漏. (2认同)