AsyncTask 真的在概念上存在缺陷还是我只是错过了一些东西?

发布于 2024-09-12 08:48:23 字数 1453 浏览 2 评论 0原文

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

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

“允许执行后台操作 操作并公布结果 UI线程无需操作 线程和/或处理程序。”

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

。原因很明显:如果触发任务的活动被破坏怎么办?这种情况可能一直发生,例如,因为您翻转了屏幕,如果任务保留对创建它的上下文的引用,那么您不仅要保留。到一个无用的上下文对象(窗口将被破坏,任何 UI 交互都会失败并出现异常!),

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

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

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

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

尽管如此,它仍然是一个复杂的解决方法,需要对一些 Droid-Fu 库类进行子类化,这使得这是一种相当侵入性的方法。

现在我只想知道:我是否只是严重遗漏了一些东西,或者 AsyncTask 真的完全有缺陷吗?您使用它的体验如何?您是如何解决这些问题的?

感谢您的意见。

I have investigated this problem for months now, came up with different solutions to it, which I am not happy with since they are all massive hacks. I still cannot believe that a class that flawed in design made it into the framework and no-one is talking about it, so I guess I just must be missing something.

The problem is with AsyncTask. According to the documentation it

"allows to perform background
operations and publish results on the
UI thread without having to manipulate
threads and/or handlers."

The example then continues to show how some exemplary showDialog() method is called in onPostExecute(). This, however, seems entirely contrived to me, because showing a dialog always needs a reference to a valid Context, and an AsyncTask must never hold a strong reference to a context object.

The reason is obvious: what if the activity gets destroyed which triggered the task? This can happen all the time, e.g. because you flipped the screen. If the task would hold a reference to the context that created it, you're not only holding on to a useless context object (the window will have been destroyed and any UI interaction will fail with an exception!), you even risk creating a memory leak.

Unless my logic is flawed here, this translates to: onPostExecute() is entirely useless, because what good is it for this method to run on the UI thread if you don't have access to any context? You can't do anything meaningful here.

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

To top it all off, even with that workaround, you still have the problem that when the context gets destroyed, you have no record of the tasks it fired. That means that you have to re-start any tasks when re-creating the context, e.g. after a screen orientation change. This is slow and wasteful.

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.

Still, it is a complex workaround and requires to sub-class some of the Droid-Fu library classes, making this a pretty intrusive approach.

Now I simply want to know: Am I just massively missing something or is AsyncTask really entirely flawed? How are your experiences working with it? How did you solve these problem?

Thanks for your input.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

懒猫 2024-09-19 08:48:23

像这样的事情怎么样:

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

How about something like this:

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(...);
    }
}
饭团 2024-09-19 08:48:23

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

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

The reason is obvious: what if the
activity gets destroyed which
triggered the task?

Manually disassociate the activity from the AsyncTask in onDestroy(). Manually re-associate the new activity to the AsyncTask in onCreate(). This requires either a static inner class or a standard Java class, plus perhaps 10 lines of code.

樱娆 2024-09-19 08:48:23

看起来AsyncTask不仅仅是概念上的缺陷。它还因兼容性问题而无法使用。 Android 文档中写道:

首次引入时,AsyncTasks 是在单个后台线程上串行执行的。 从 DONUT 开始,这被更改为允许多个任务并行操作的线程池。< /em> 启动 HONEYCOMB,任务又回到在单线程上执行,以避免并行执行引起的常见应用程序错误。 如果您确实想要并行执行,可以使用 > executeOnExecutor(Executor, Params...) 此方法的版本 THREAD_POOL_EXECUTOR; 但是,请参阅其中有关其使用警告的评论。

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

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

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

此外,文档说:

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

It looks like AsyncTask is a bit more than just conceptually flawed. It is also unusable by compatibility issues. The Android docs read:

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.

Both executeOnExecutor() and THREAD_POOL_EXECUTOR are Added in API level 11 (Android 3.0.x, HONEYCOMB).

This means that if you create two AsyncTasks to download two files, the 2nd download will not start until the first one finishes. If you chat via two servers, and the first server is down, you will not connect to the second one before the connection to the first one times out. (Unless you use the new API11 features, of course, but this will make your code incompatible with 2.x).

And if you want to target both 2.x and 3.0+, the stuff becomes really tricky.

In addition, the docs say:

Caution: Another problem you might encounter when using a worker thread is unexpected restarts in your activity due to a runtime configuration change (such as when the user changes the screen orientation), which may destroy your worker thread. To see how you can persist your task during one of these restarts and how to properly cancel the task when the activity is destroyed, see the source code for the Shelves sample application.

风轻花落早 2024-09-19 08:48:23

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

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

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

Probably we all, including Google, are misusing AsyncTask from the MVC point of view.

An Activity is a Controller, and the controller should not start operations that may outlive the View. That is, AsyncTasks should be used from Model, from a class that is not bound to the Activity life cycle -- remember that Activities are destroyed on rotation. (As to the View, you don't usually program classes derived from e.g. android.widget.Button, but you can. Usually, the only thing you do about the View is the xml.)

In other words, it is wrong to place AsyncTask derivatives in the methods of Activities. OTOH, if we must not use AsyncTasks in Activities, AsyncTask loses its attractiveness: it used to be advertised as a quick and easy fix.

但可醉心 2024-09-19 08:48:23

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

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

I'm not sure it's true that you risk a memory leak with a reference to a context from an AsyncTask.

The usual way of implementing them is to create a new AsyncTask instance within the scope of one of the Activity's methods. So if the activity is destroyed, then once the AsyncTask completes won't it be unreachable and then eligible for garbage collection? So the reference to the activity won't matter because the AsyncTask itself won't hang around.

空名 2024-09-19 08:48:23

对您的活动保留 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]);
            }
        }
    }

}

It would be more robust to keep a WeekReference on your activity :

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

}
跨年 2024-09-19 08:48:23

为什么不直接重写所属 Activity 中的 onPause() 方法并从那里取消 AsyncTask 呢?

Why not just override the onPause() method in the owning Activity and cancel the AsyncTask from there?

夏日落 2024-09-19 08:48:23

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

You are absolutely right - that is why a movement away from using async tasks/loaders in the activities to fetch data is gaining momentum. One of the new ways is to use a Volley framework that essentially provides a callback once the data is ready - much more consistent with MVC model. Volley was populised in the Google I/O 2013. Not sure why more people aren't aware of this.

鹿港巷口少年归 2024-09-19 08:48:23

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

Personally, I just extend Thread and use a callback interface to update the UI. I could never get AsyncTask to work right without FC issues. I also use a non blocking queue to manage the execution pool.

ゃ人海孤独症 2024-09-19 08:48:23

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

在这里他们 RTFMing 对此:

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

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

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

I thought cancel works but it doesn't.

here they RTFMing about it:

""If the task has already started, then the mayInterruptIfRunning
parameter determines whether the thread executing this task should be
interrupted in an attempt to stop the task."

That does not imply, however, that the thread is interruptible. That's a
Java thing, not an AsyncTask thing."

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

灼疼热情 2024-09-19 08:48:23

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

确保您的生命周期中有取消策略,以便它最终会被垃圾收集,并且不再保留对您的活动的引用,并且它也可以被垃圾收集。

如果在离开 Context 时取消 AsyncTask,您将遇到内存泄漏和 NullPointerExceptions,如果您只需要提供诸如 Toast 之类的简单对话框之类的反馈,那么应用程序上下文的单例将有助于避免 NPE 问题。

AsyncTask 并不全是坏事,但肯定有很多神奇的事情发生,可能会导致一些不可预见的陷阱。

You would be better off thinking of AsyncTask as something that is more tightly coupled with an Activity, Context, ContextWrapper, etc. It's more of a convenience when its scope is fully understood.

Ensure that you have a cancellation policy in your lifecycle so that it will eventually be garbage collected and no longer keeps a reference to your activity and it too can be garbage collected.

Without canceling your AsyncTask while traversing away from your Context you will run into memory leaks and NullPointerExceptions, if you simply need to provide feedback like a Toast a simple dialog then a singleton of your Application Context would help avoid the NPE issue.

AsyncTask isn't all bad but there's definitely a lot of magic going on that can lead to some unforeseen pitfalls.

老娘不死你永远是小三 2024-09-19 08:48:23

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

As to "experiences working with it": it is possible to kill the process along with all AsyncTasks, Android will re-create the activity stack so that the user will not mention anything.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文