Android AsyncTask 真的在概念上有缺陷还是我只是遗漏了什么?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3357477/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-20 09:53:07  来源:igfitidea点击:

Is AsyncTask really conceptually flawed or am I just missing something?

androidconcurrencyhandlerandroid-asynctask

提问by Matthias

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

问题在于AsyncTask. 根据文档它

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

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

The example then continues to show how some exemplary showDialog()method is called in onPostExecute(). This, however, seems entirely contrivedto 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.

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

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 anyUI interaction will fail with an exception!), you even risk creating a memory leak.

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

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.

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

One workaround would be to not pass context instances to an AsyncTask, but a Handlerinstance. 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).

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

To top it all off, even with that workaround, you still have the problem that when the context gets destroyed, you have no recordof 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 andyou 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 库中实现)是WeakReference在唯一的应用程序对象上维护从组件名称到其当前实例的s映射。每当启动 AsyncTask 时,它都会在该映射中记录调用上下文,并且在每次回调时,它将从该映射中获取当前上下文实例。这确保您永远不会引用过时的上下文实例,并且您始终可以访问回调中的有效上下文,以便您可以在那里进行有意义的 UI 工作。它也不会泄漏,因为引用很弱,并且在不再存在给定组件的实例时被清除。

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

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

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?

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

Thanks for your input.

感谢您的输入。

采纳答案by hackbod

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

回答by CommonsWare

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

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

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

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

回答by 18446744073709551615

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

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

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 theexecuteOnExecutor(Executor, Params...)version of this method withTHREAD_POOL_EXECUTOR; however, see commentary there for warnings on its use.

首次引入时,AsyncTasks 在单个后台线程上串行执行。从 DONUT 开始,这已更改为允许多个任务并行操作的线程池。启动HONEYCOMB,任务回到单线程执行,避免并行执行导致的常见应用错误。如果你真的想要并行执行,你可以使用executeOnExecutor(Executor, Params...)这个方法版本THREAD_POOL_EXECUTOR但是,请参阅那里的评论以了解有关其使用的警告。

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

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

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

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

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

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

In addition, the docssay:

此外,文档说:

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.

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

回答by 18446744073709551615

Probably we all, including Google, are misusing AsyncTaskfrom the MVCpoint of view.

也许,我们都,包括谷歌,被滥用AsyncTaskMVC的观点。

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 Viewis the xml.)

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

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

回答by oli

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

我不确定您是否会因引用 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.

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

回答by Snicolas

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

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

}

回答by Jeff Axelrod

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

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

回答by C0D3LIC1OU5

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

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

回答by jtuchek

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.

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

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,您将遇到内存泄漏和 NullPointerExceptions,如果您只需要提供像 Toast 这样的反馈一个简单的对话框,那么应用程序上下文的单例将有助于避免 NPE 问题。

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

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

回答by androidworkz

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.

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