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

时间:2010-07-28 21:04:52

标签: android concurrency handler android-asynctask

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

问题在于AsyncTask。根据文件

  

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

然后,该示例继续显示如何在showDialog()中调用一些示例性onPostExecute()方法。但是,对我来说,这似乎是完全是设计的,因为显示一个对话框始终需要对有效Context的引用,并且AsyncTask 必须永远不能对上下文对象保持强引用

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

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

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

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

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

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

现在我只想知道:我是否只是大量遗漏某些内容,或者AsyncTask是否真的完全有缺陷?您的使用经验如何?你是怎么解决这些问题的?

感谢您的意见。

12 个答案:

答案 0 :(得分:85)

这样的事情怎么样:

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

答案 1 :(得分:20)

  

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

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

答案 2 :(得分:15)

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

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

{1}}和THREAD_POOL_EXECUTOR 在API级别11 (Android 3.0.x,HONEYCOMB)中添加。

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

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

此外,docs说:

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

答案 3 :(得分:12)

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

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

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

答案 4 :(得分:5)

我不确定通过引用AsyncTask中的上下文会冒内存泄漏的风险。

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

答案 5 :(得分: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]);
            }
        }
    }

}

答案 6 :(得分:1)

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

答案 7 :(得分:1)

你是绝对正确的 - 这就是为什么在活动中使用异步任务/加载器来获取数据的动作正在获得动力。其中一种新方法是使用Volley框架,该框架在数据准备好后基本上提供回调 - 与MVC模型更加一致。 Volley在Google I / O 2013中大量涌现。不知道为什么更多人不知道这一点。

答案 8 :(得分:0)

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

答案 9 :(得分:0)

我认为取消有效,但事实并非如此。

在这里他们RTFMing关于它:

“”如果任务已经启动,那么mayInterruptIfRunning 参数确定执行此任务的线程是否应该是 因试图停止任务而中断。“

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

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

答案 10 :(得分:0)

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

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

在不离开Context的情况下取消AsyncTask会导致内存泄漏和NullPointerExceptions,如果您只需要像Toast这样的简单对话框提供反馈,那么应用程序上下文的单例将有助于避免NPE问题。 / p>

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

答案 11 :(得分:-1)

关于“使用它的体验”:对于所有AsyncTasks, 终止进程 possible,Android将重新创建活动堆栈所以用户不会提及任何内容。

相关问题