我已经调查了这个问题几个月了,提出了不同的解决方案,我不满意,因为它们都是巨大的黑客。我仍然无法相信一个在设计上有缺陷的类被纳入框架,而没有人谈论它,所以我想我一定是错过了什么。

问题出在AsyncTask上。根据文档它

允许执行后台 操作并在 UI线程,而不需要操作 线程和/或处理程序。

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

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

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

One workaround would be to not pass context instances to an AsyncTask, but a Handler instance. That works: since a Handler loosely binds the context and the task, you can exchange messages between them without risking a leak (right?). But that would mean that the premise of AsyncTask, namely that you don't need to bother with handlers, is wrong. It also seems like abusing Handler, since you are sending and receiving messages on the same thread (you create it on the UI thread and send through it in onPostExecute() which is also executed on the UI thread).

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

My solution to this (as implemented in the Droid-Fu library) is to maintain a mapping of WeakReferences from component names to their current instances on the unique application object. Whenever an AsyncTask is started, it records the calling context in that map, and on every callback, it will fetch the current context instance from that mapping. This ensures that you will never reference a stale context instance and you always have access to a valid context in the callbacks so you can do meaningful UI work there. It also doesn't leak, because the references are weak and are cleared when no instance of a given component exists anymore.

不过,这是一种复杂的解决方法,需要子类化一些Droid-Fu库类,这是一种相当具有侵入性的方法。

现在我只是想知道:我只是大量的遗漏了一些东西或AsyncTask真的完全有缺陷?你的工作经历如何?你是如何解决这些问题的?

谢谢你的建议。


当前回答

这样怎么样:

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

其他回答

至于“使用它的经验”:有可能杀死进程连同所有AsyncTasks, Android将重新创建活动堆栈,这样用户就不会提及任何事情。

这样怎么样:

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

我不确定从AsyncTask引用上下文是否存在内存泄漏的风险。

实现它们的通常方法是在Activity的一个方法的范围内创建一个新的AsyncTask实例。所以如果活动被销毁,那么一旦AsyncTask完成,它不会是不可达的,然后有资格进行垃圾收集吗?因此对活动的引用并不重要,因为AsyncTask本身不会挂在那里。

可能我们所有人,包括谷歌,都从MVC的角度误用了AsyncTask。

一个活动是一个控制器,控制器不应该启动可能比视图活得更久的操作。也就是说,AsyncTasks应该从Model中使用,从一个不绑定到Activity生命周期的类中使用——记住,Activity在旋转时被销毁。(至于视图,你通常不编写从android.widget派生的类。纽扣,但你可以。通常,你对View所做的唯一一件事就是xml。)

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

看起来AsyncTask不仅仅是概念上的缺陷。由于兼容性问题,它也无法使用。安卓文档中写道:

When first introduced, AsyncTasks were executed serially on a single background thread. Starting with DONUT, this was changed to a pool of threads allowing multiple tasks to operate in parallel. Starting HONEYCOMB, tasks are back to being executed on a single thread to avoid common application errors caused by parallel execution. If you truly want parallel execution, you can use the executeOnExecutor(Executor, Params...) version of this method with THREAD_POOL_EXECUTOR; however, see commentary there for warnings on its use.

executeOnExecutor()和THREAD_POOL_EXECUTOR都是在API级别11 (Android 3.0)中添加的。x,蜂窝)。

这意味着如果你创建了两个asynctask来下载两个文件,第二次下载将在第一次下载完成后才开始。如果您通过两个服务器聊天,并且第一个服务器宕机,那么在第一个服务器连接超时之前,您将不会连接到第二个服务器。(当然,除非您使用新的API11特性,但这会使您的代码与2.x不兼容)。

如果你想同时瞄准2。X和3.0+,这些东西变得非常棘手。

此外,医生说:

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