【搬运】AsyncTask is Deprecated, Now What?

原文链接,翻译如下(不重要的内容我没有严格逐句翻译)

------------------------------------------------------------------------------------------------------------------------------------------------------

AysncTask一直非常有争议,它应用广泛,但同时大多数专业的Android开发人员并不喜欢它。

总而言之,Android社区与AsyncTask爱恨交织。不过AsyncTask的时代即将结束,因为deprecated的提交刚刚并入了Android Open Source Project。

在这篇文章中,我将回顾促使AsyncTask弃用的官方声明,以及必须弃用AsyncTask的真正原因。 此外,在本文结尾处,我将分享我对Android并发API的未来的看法。

一、官方说法

此次提交中标明了AsyncTask的正式弃用以及该决定的动机。:

AsyncTask was intended to enable proper and easy use of the UI thread.
However, the most common use case was for integrating into UI, 
and that would cause Context leaks, missed callbacks, 
or crashes on configuration changes. 
It also has inconsistent behavior on different versions of the platform,
swallows exceptions from doInBackground, 
and does not provide much utility over using Executors directly.

虽然这是Google的官方声明,但还是有些错漏:

首先,AsyncTask的目的并不是“启用适当且容易使用的UI线程”。它的目的是:将长时间运行的操作从UI线程转移到后台线程,然后将这些操作的结果传递回UI线程。我的想法可能太挑剔,但是我认为,这个API Google自己开发和推广了很多年,使用得太广泛了,所以Google其实应该多花些精力推广相关的弃用消息。

也就是说,弃用声明里“that would cause Context leaks, missed callbacks”这句话该展开讨论。但Google只是指出了AsyncTask的会触发非常严重的问题的最常见用例。有许多高质量应用程序也使用AsyncTask,而且是可以正常工作的。甚至AOSP内部的某些类也使用AsyncTask。他们怎么没有遇到这些问题?

为了回答这个问题,让我们详细讨论AsyncTask和内存泄漏之间的关系。

AsyncTask和内存泄漏

下面这个例子中,AsyncTask使得Fragment(或Activity)对象永远无法回收,也就是造成了内存泄漏:

private final AtomicInteger counter = new AtomicInteger(0);
 
@Override
public void onStart() {
    super.onStart();
    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... voids) {
            while (true) {
                Log.d("AsyncTask", "count: " + counter.get());
                counter.incrementAndGet();
            }
        }
    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

看起来这个例子证明了Google的观点:AsyncTask确实会导致内存泄漏。 我们应该使用其他方法来编写并发代码! 好吧,让我们尝试一下。

这是使用RxJava重写的相同示例:

@Override
public void onStart() {
    super.onStart();
    Observable.fromCallable(() -> {
        while (true) {
            Log.d("RxJava", "count: " + counter.get);
            counter.incrementAndGet();
        }
    }).subscribeOn(Schedulers.computation()).subscribe();
}

它也造成了内存泄漏

也许Kotlin Coroutines不会?以下使用Kotlin Coroutines实现相同功能的方式:

override fun onStart() {
    super.onStart()
    CoroutineScope(Dispatchers.Main).launch {
        withContext(Dispatchers.Default) {
            while (true) {
                Log.d("Coroutines", "count: ${counter.get()}")
                counter.incrementAndGet()
            }
        }
    }
}

不幸的是,它会导致完全相同的内存泄漏。

这样看来,此功能就是会泄漏封闭的Fragment(或Activity),不管选择多线程框架是怎么写的。 实际上,即使我使用Thread,也可能导致泄漏:

@Override
public void onStart() {
    super.onStart();
    new Thread(() -> {
        while (true) {
            Log.d("Thread", "count: " + counter.get());
            counter.incrementAndGet();
        }
    }).start();
}

因此,毕竟,这与AsyncTask无关,而与我编写的逻辑有关。 为了说明这一点,让我们修改使用AsyncTask修复内存泄漏的示例:

private AsyncTask mAsyncTask;
 
@Override
public void onStart() {
    super.onStart();
    mAsyncTask = new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... voids) {
            while (!isCancelled()) {
                Log.d("AsyncTask", "count: " + counter.get());
                counter.incrementAndGet();
            }
            return null;
        }
    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
 
@Override
public void onStop() {
    super.onStop();
    mAsyncTask.cancel(true);
}

在这种情况下,我使用了可怕的AsyncTask,但是没有泄漏。真神奇呢。

好吧,没什么神奇的。这只是说明AsyncTask是可以编写出安全且正确的多线程代码,就像您可以使用其他任何多线程框架一样。 AsyncTask和内存泄漏之间没有特殊关系。因此,普遍认为的AsyncTask自动导致内存泄漏以及AOSP中的新弃用消息是完全错误的。

您现在可能想知道:AsyncTask导致内存泄漏的想法既然并不准确,那为什么它在Android开发人员中如此普遍?

好吧,Android Studio中有一个内置的Lint规则,它会警告您并建议您将AsyncTasks设置为静态以避免内存泄漏。该警告和建议也不正确,但是在他们的项目中使用AsyncTask的开发人员会收到此警告,并且由于它来自Google,因此大家都这么认为了。

这个Lint警告也是让误导大家把AsyncTask与内存泄漏联系在一起——Google本身已将这个联系强加给开发人员。

 

如何避免多线程代码中的内存泄漏

到目前为止,我们确定AsyncTask的使用与内存泄漏之间没有必然的因果关系。而且会发现任何多线程框架都可能发生内存泄漏。我想现在你可能想知道如何避免发生内存泄漏。

我不会详细回答这个问题,因为我想保持话题,但是我也不想让你空手而归,以下是Java和Kotlin中编写内存泄漏免费多线程代码所需了解的概念:

垃圾收集器
垃圾回收的根源
关于垃圾回收的线程生命周期
从内部类到父对象的隐式引用
如果您详细了解这些概念,则会大大降低代码中内存泄漏的可能性。另一方面,如果您不理解这些概念并编写并发代码,那么无论使用哪种多线程框架,引入内存泄漏都只是时间问题。

由于这是所有Android开发人员的基础知识和重要知识,因此我决定将Android Multithreading Masterclass课程的第一部分上传到YouTube。与官方文档相比,它涵盖了上述主题。您可以在这里免费观看。

AsyncTask 是被无故弃用的吗

AsyncTask不会自动导致内存泄漏,所以Google是无故弃用它?好吧,不完全是。

在过去的几年中,AsyncTask已被Android开发人员自己“实际弃用”。我们许多人公开反对在应用程序中使用此API。对于那些维护广泛使用AsyncTask的代码库的开发人员,我个人感到抱歉。如果您问我,我想说Google应该早就弃用了。

因此,尽管Google仍然对自己的创作感到困惑,但弃用本身是非常合适和受欢迎的。至少,它将使新的Android开发人员知道他们不需要花费时间来学习此API,也不会在应用程序中使用它。

说了这么多,您可能仍然不明白为什么AsyncTask是“不好的”,以及为什么这么多的开发人员如此讨厌它。我认为这是一个非常有趣且实用的问题。毕竟,如果我们不了解AsyncTask到底是什么问题,则不能保证我们不会再次重复相同的错误。

因此,让我列出我个人认为真正的AsyncTask的不足之处。

AsyncTask Problem 1: 使多线程更加复杂

AsyncTask的主要“卖点”始终是保证您自己无需处理Thread类和其他多线程原语。它本应该使多线程更简单,尤其是对于新的Android开发人员而言。听起来不错,对吗?但是,实际上,这种“简单性”适得其反。

AsyncTask的类级Javadoc使用“线程”一词16次。如果您不了解什么是线程,就根本无法理解AsyncTask。另外,此Javadoc声明了许多特定于AsyncTask的约束和条件。换句话说,如果您想使用AsyncTask,则需要了解线程,并且还需要了解AsyncTask本身的许多细节。凭空想象,这并不“简单”。

此外,多线程本质上是复杂的主题。在我看来,这是一般软件(就此而言,也是硬件)中最复杂的主题之一。现在,与许多其他概念不同,您无法在多线程中采用捷径,因为即使是最小的错误也可能导致非常严重的错误,这将非常难以调查。即使在开发人员知道它们存在之后,仍有数月来受到多线程错误影响的应用程序。他们只是找不到这些错误。

因此,我认为根本无法简化并发,而AsyncTask的野心注定从一开始就失败了。

AsyncTask Problem 2: 糟糕的文档

Android文档不是最佳文档(在此要保持礼貌)并不是什么秘密。这些年来,情况有所改善,但即使到今天,我也觉得它好。我认为,糟糕的文档也是使AsyncTask陷入了困境的因素。如果AsyncTask只是按原样过度设计,成为一个复杂且有很多细节的多线程框架,但是有了良好的文档说明,那他可能还是会被保留。毕竟,Android开发人员已经习惯了丑陋的API。但是AsyncTask的文档非常糟糕,并使所有其他缺陷更加严重。

demo中编写多线程代码的方法很糟糕:所有代码都在Activity中,完全忽略生命周期,不讨论onCancel时该做的事情等。如果您在自己的应用程序中使用这些示例,几乎必然会导致内存泄漏。

此外,AsyncTask的文档没有包含与多线程相关的核心概念的任何解释(我之前列出的概念以及其他概念)。实际上,我认为官方文档中没有任何内容,甚至仅仅是指向JLS第17章:线程和锁,这是Oracle文档不是Android的官方文档。

顺便说一下,在我看来,前面提到的Android Studio中的Lint规则(也是散布有关内存泄漏的神话)也是该文档的一部分。因此,不仅文档不足,而且包含不正确的信息。

AsyncTask Problem 3: 过于复杂

AsyncTask具有三个通用参数。 三个! 如果我没记错的话,我从来没有看过任何其他需要这么多泛型的类。

我仍然记得我第一次接触AsyncTask。 到那时,我已经对Java线程有所了解,并且不明白为什么Android中的多线程如此困难。 三个泛型很难理解,使我非常紧张。 此外,由于AsyncTask的方法是在不同的线程上调用的,因此我不得不不断地提醒自己有关问题,然后通过阅读文档来验证是否正确。

今天,当我对并发和Android的UI线程有了更多了解时,我可能可以对这些信息进行反向工程。 但是,在我完全放弃了AsyncTask之后,这种理解水平才在我的职业生涯的后期出现。

虽然这么复杂,开发人员却依然可以从UI线程简单地调用execute()!

AsyncTask Problem 4: 继承的滥用

AsyncTask是基于继承的:只要您需要在后台执行任务,就可以扩展AsyncTask。

与糟糕的文档相结合,继承将开发人员推向了编写巨大类的方向,这些类以最低效且难以维护的方式将多线程,域和UI逻辑耦合在一起。 毕竟,为什么不呢? 这就是AsyncTask的API所适合的。

如果遵循有效Java的“从继承中的偏重组成”规则,则对于AsyncTask而言,将产生真正的不同。 [有趣的是,Effective Java的作者Joshua Bloch在Google工作,并在相对较早的阶段就涉足Android。

AsyncTask Problem 5: 可靠性

简而言之,默认THREAD_POOL_EXECUTOR导致了AsyncTask的配置错误且不可靠。 多年来,Google至少两次对其配置进行了调整(该提交该提交),但仍使官方的Android设置应用崩溃。
大多数Android应用程序永远都不需要这种级别的并发性。 但是,您永远都不知道一年后的用例是什么,因此依靠不可靠的解决方案是有问题的。

AsyncTask Problem 6: Concurrency Misconception

这一点与不良的文档有关,但我认为它本身应该是一个重点。 executeOnExecutor()方法的Javadoc指出:

Allowing multiple tasks to run in parallel from a thread pool is generally not what one wants, because the order of their operation is not defined. […] Such changes are best executed in serial; to guarantee such work is serialized regardless of platform version you can use this function with SERIAL_EXECUTOR

通常不希望从线程池中并行运行多个任务,因为未定义其操作顺序。 […]这种更改最好以串行方式执行; 为了确保无论平台版本如何,都可以序列化此类工作,您可以将此功能与SERIAL_EXECUTOR一起使用

好吧,这是错误的。 在大多数情况下,从UI线程开启并发时,正是希望允许多个任务同时运行。

例如,假设您发送了一个网络请求,但由于某种原因它超时了。 OkHttp中的默认超时为10秒。如果您确实使用SERIAL_EXECUTOR(在任何给定的瞬间仅执行一项任务),那么您刚刚停止了应用程序中的所有后台工作10秒钟。如果您碰巧发送了两个请求并且都超时了?好吧,20秒没有后台处理。现在,超时网络请求已不再是一种例外,几乎在所有其他用例中都一样:数据库查询,图像处理,计算,IPC等。

是的,如文档中所述,操作顺序是不确定的,因为它们可以同时运行。但这不是问题啊,实际上,这可以说是并发的定义。

因此,我认为,官方文档中的这一说法引起了误解,我没有看到对官方文档中此类误导性信息的其他解释。

AsyncTask的未来

希望我说服您,弃用AsyncTask是Google的一个好举动。 但是,对于今天使用AsyncTask的项目来说,这些并不是好消息。 如果您从事此类项目,是否应该立即重构代码?

首先,我认为您不需要从代码中主动删除AsyncTasks。 弃用该API并不意味着它将停止工作。 实际上,只要AsyncTask能够在Android上生存那么久,我都不会感到惊讶。 该API太多,包括Google自己的应用程序。 即使将其删除(例如在5年之内),您也可以将其代码复制粘贴到自己的项目中,并更改import语句以保持逻辑正常运行。

此弃用的主要影响将对新的Android开发人员。 对他们来说不需要花时间学习AsyncTask,也不会在新的应用程序中使用它。

Android多线程的未来

AsyncTask的弃用留下了一些空白,其他一些多线程方法会填补空白。 应该是什么呢? 让我与您分享我对此主题的看法。

首先,我认为您不需要从代码中主动删除AsyncTasks。 弃用该API并不意味着它将停止工作。 实际上,就算AsyncTask一直存在着我都不会感到惊讶,因为包括Google自己开发的APP在内,很多APP都有用到这个API。 即使将其删除(例如在5年之内),您也可以将其代码复制粘贴到自己的项目中,并更改import语句以保持逻辑正常运行。此弃用的主要影响将对新的Android开发人员。 对他们来说很清楚,他们不需要花时间学习AsyncTask,也不会在新的应用程序中使用它。

如果您只是刚开始用Android,建议您使用Thread类和UI Handler的组合。 许多Android开发人员都会反对,但我本人使用这种方法已有一段时间,并且它的工作原理比AsyncTask更好,而且要好得多。

结论

在我看来,早就应该弃用AsyncTask,这有利于整理Android中的多线程环境。多年来,这个API存在许多问题,并造成了很多麻烦。

不幸的是,正式的弃用声明包含不正确的信息,并可能使今天使用AsyncTask的开发人员感到困惑,或者将来会继承AsyncTask的代码库。希望这篇文章专门阐明了有关AsyncTask的一些观点,并且还为您提供了关于Android并发性的一般思考。

对于正在使用AsyncTask的项目,可以不需要立即采取任何措施, AsyncTask不会很快从Android中删除。

猜你喜欢

转载自blog.csdn.net/qq_33298609/article/details/108622856
今日推荐