震惊!AsyncTask将被弃用?

AsyncTask被弃用了,怎么办?

这是篇翻译自 Vasiliy的文章

原文地址https://www.techyourchance.com/asynctask-deprecated/

在过去的十年里,AsyncTask一直是Android并发 代码开发中最广为使用的解决方案。 然而,它备受争议。一方面,AysncTask很强大,并且在大量的Android应用中依然很好用,另一方面,很多专业Adnroid开发者公开表示不喜欢这个API。

总之,我想说Adnroid社区对AsyncTask又爱又恨。但现在有了个大新闻:AsyncTask的时代要结束了

因为AOSP落实了它将被弃用的commit提交。

在这篇文章中我会评论一下AysncTask被启用的官方动机和它为什么被弃用的真正原因。你将会看到,有一系列不同的原因。另外在这篇文章结尾我会分享自己关于未来Adnroid并发相关的API的想法。

AsyncTask被弃用的官方原因

这个commit中介绍了官方对于AsyncTask的弃用以及对于这个做出决定的动机。新添加的Javadoc第一段指出:

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.

AysncTask意图提供简单且合适的UI线程使用,然而,它最主要的使用实例是作为UI的组成部分,会导致内存泄漏,回调遗失或改变配置时崩溃。且它在不同版本的平台上有不一致的行为,吞下来自doInBackground的异常,并且不能提供比直接使用Executors更多的功能。

这是来自谷歌官方的声明,在这里要指出几个它的不合理之处。

首先,AsyncTask从来没有过从来没有过意图”提供简单且合适的UI线程使用“。它适用于减少UI线程中的长耗时操作到后台线程中,并且传递这些操作结果回UI线程。我想我在这儿有点吹毛求疵,但在我的观点中,当谷歌要弃用一个自己开发并维护了这么多年的API时,这样会对那些今天还在使用这个API并且将来纪念可能还会继续使用的开发者显得更加尊重,投入更多精力到API的弃用说明信息中会避免大家更多地困惑。

“译者按:在查阅之后,译者有发现官网中关于AsyncTask提到<AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask. > 官方有提议AsyncTask只作为UI线程的辅助类而不通用, 理想情况下只用于短操作(最多几秒钟),长耗时操作还是应该使用java.util.concurrent包,所以作者在这里犯了个小错误,而更多的是开发者们对AsyncTask的误用。“

在这段弃用说明中更有趣的部分在这里”导致内存泄漏,回调遗失或改变配置时崩溃“,谷歌仅基本地指出了广泛的使用AsyncTask会自动的造成很严重的问题。然而,很多高质量的应用都使用了AsyncTask却工作的很完美并没有造成泄漏。甚至很多AOSP自己的内部类也使用了AsyncTask,为什么它们没有出现这些问题?

为了回答这个问题,我们来讨论一下AsyncTask和内存泄漏在细节上的关系。

AsyncTask和内存泄漏

这个AsyncTask永远无法关闭Fragment (或Activity) 实例从而造成内存泄露:

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

看起来这个示例证实了谷歌的观点:AsyncTask确实引起内存泄漏。我们应该使用更多的方式实现这段并发代码!下面,我们给出一个尝试。

这是同一个示例,使用RxJava重写:

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

他同样不能关闭Fragment (或Activity)引起了泄漏。

也许全新的Kotlin协程能有所帮助?这是我如何使用协程实现了同样的功能:

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

不幸的是,结果依然造成了内存泄漏。

观察无法关闭Fragment (或Activity)造成泄漏的特征,先忽略关于多线程框架的选择。实际上,我直接使用Thread类依然会造成泄漏:

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

所以,这与AsyncTask无关,而是归咎于我所写的代码的逻辑。为了说明这一点,我们改进这个示例并用AsyncTask来修复内存泄漏:

private AsyncTask mAsyncTask;
 
@Override
public void onStart() {
    super.onStart();
    mAsyncTask = new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... voids) {
            int counter = 0;
            while (!isCancelled()) {
                Log.d("AsyncTask", "count: " + counter);
                counter ++;
            }
            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 中会有一个连接规则提醒并建议你将AsyncTask静态化以避免内存泄漏。这个警告和建议同样是错误的,但在项目中使用AsyncTask的开发者常常在得到这个警告后因为它来自谷歌而接受这个建议。

在这里插入图片描述

在我想来,这个警示可能是大家广泛将AsyncTask和内存泄漏关联起来而原因——是被谷歌自己给强加给开发人员的。

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

到现在我们已经达成了AsyncTask和内存泄漏没有必然联系的共识。另外,你看到了内存泄漏可能发生在任何多线程框架中。

我不会详细的回答这个问题,因为我不想偏题,但我同样不想让你空手而归。因此,让我列几条在你需要理解的原则以避免Java或Kotlin多线程开发中的内存泄漏:

  • Garbage collector 垃圾回收器
  • Garbage collection roots 垃圾回收的根源
  • Thread lifecycle in respect to garbage collection 相对于垃圾回收的线程生命周期
  • Implicit references from inner classes to parent objects 来自内部类和父类的隐性影响

如果你能详细的理解这些原则,你会在你的代码中相当程度的避免内存泄漏的发生。相反,如果你写并发代码时不能理解这些原则,无论使用任何多线程框架造成内存泄漏都只是时间问题。

作者按:

因为这是多与所有Android开发者很基础而且很重要的知识,我决定在我Youtube上的Android多线程开发课程中上传第一部分的课程,这比官方文档更详细的涵盖了了之前所提到主题,你可以免费观看。[https://www.youtube.com/watch?v=UPq1LDxL5_w&feature=youtu.be]

AsyncTask是被无缘由弃用的吗

因为AsyncTask并非自动的引起内存泄漏,看起来谷歌是错误的弃用了它,没有任何原因的。这种说法并不正确。

在过去的几年里,AsyncTask已经被广大Adnroid开发者“事实上弃用“。我们大多数都在公开的反对在应用中使用这个API。我个人对那些在维护代码库时依然广泛使用的AsyncTask的开发者感到遗憾。很多年来,AsyncTask已经被证实是一个很有问题的API。因此,AsyncTask的弃用不仅仅是符合逻辑的,如果你问我,我还会说谷歌早就该把它弃用了。

因而,在谷歌还在为他们自己的开发感到困惑时,这个弃用已经受到了非常多的欢迎和感谢。至少,这会让新的Android开发者知道他们不需要花太多时间去学习这个API或将它使用在自己的项目中。

说到这,你可能依然不懂为啥AsyncTask是“恶劣的”并且这么多开发者如此的厌恶它。在我想来,这是一个非常有趣且实用的疑惑。总之,如果我不能理解AsyncTask到底有什么问题,就不能保证我不再犯同样的错误。

因此,让我列举出我个人所看到的AsyncTask的真正不足之处。

AsyncTask的问题一:使得多线程更加复杂

AsyncTask的一大卖点在于承诺你不再需要亲自处理Thread类和其他原生多线程类。这会使得多线程更加简单,尤其是对于Android初学者。听起来很棒,对吗?然而,这个“简化”事与愿违。

AsyncTask’s class-level Javadoc使用了“Thread"这个词16次,显然你无法理解它如果你不能理解线程是什么。另外,这份文档声明了一系列AsyncTask特有的约束和条件。换句话说,如果你想用AsyncTask,你需要理解Thread并且还要理解很多AsyncTask自有的细微差别。这完全不像想象中的那么”简单"。

更何况,多线程变成本质上就是一个非常复杂的主题。在我想来,一般而言这应该是软件中最为复杂的主题之一(就此而言对于硬件也是同样的)。现在,你能像其他的概念一样在多线程编程中寻求捷径,因为即使是最小的错误也可能引起非常严重的bug,并且会及其难以寻找。有很多示例应用已经投入使用数月了开发者才发现有多线程的bug,并且依然无法找到bug具体在哪里。

因此在我想来,并没有方法能够简化并发问题并且AsyncTask的目标从一开始就错了。

AsyncTask的问题二:糟糕的文档

Android的文档不是很棒已经不是一个秘密了(在这儿我尝试能更礼貌些)。即便这些年来情况有所改善,但即使到今天我也不会称其为得体。我想糟糕的文档决定了AsyncTask问题多发的历史。如果AsyncTask仅仅是被过度设计,复杂且细微的多线程框架,就像它现在那样,但有一个很好的文档,它可能仍然作为生态系统的一部分被保留。总之,Android开发者们并非难以习惯丑陋的API,只是AsyncTask灾难般的文档使得它的缺陷更为突出。

最差劲的是它所提供的案例,它展示了编写多线程代码最为不幸的方式:所有的代码都在Activity中,完全忽视了生命周期,没有任何关于取消方案的讨论,如此种种。如果你在自己的应用中使用了这些案例,内存泄漏和错误的行为会如期而至。

另外,AsyncTask的文档中不包含任何关于多线程编程核心概念的说明(比如我之前所列举的几条还有其他)。事实上我想这份官方文档中没有任何一部分有做到。甚至没有提供给真正想要通过“官方”参考了解并发问题的开发者一个通往 JLS Chapter 17: Threads and Locks的连接(打引号的原因是Oracle 的文档对于Android而言并非官方)。

顺便提一下,我想之前提到的Android Studio中广泛传播了关于AsyncTask会导致内存泄漏传说的代码检查规则,这依然是文档中的一部分,因此,不仅仅是文档不够充分,而且还包含了错误的信息。

AsyncTask的问题三:太过复杂

AsyncTask有三个通用参数。三个!如果我没搞错,我从没见过其他的类需要这么多参数。

我还记得我第一看见 AsyncTask。那是我已经学会了一点Java线程但不能理解为啥Android中的多线程会那么难。三个通用的参数对于我来说有点儿太难理解并搞得我很紧张。另外,自从AsyncTask的方法被不同的线程调用,我需要始终提醒自己关于这点然后通过阅读文档验证自己是否正确。

现在我对并发和Android的UI线程的理解深入了很多,我可以对这些信息进行反向推理。然而,到达这个层次是我进入职业生涯很多年之后了,尤其在我完全被AsyncTask坑过之后。

尽管很复杂,你依然需要只通过UI线程调用execute() 。

AsyncTask的问题四:滥用继承

AsyncTask的设计理念基于继承:任何时候你需要在后台执行一个任务,你都需要继承AsyncTask。

结合糟糕的文档而言,继承的设计理念使得开发者偏向于编写大型类,这些类以最低效且难于维护的方式将多线程,域和UI逻辑耦合在一起。而这恰是AsyncTask的API所引导的。

Effective Java推崇“使用组合而不是继承”原则,如果遵循,AsyncTask会造成完全不同的情况。【有趣的是,Effective Java的作者Joshua Bloch,在谷歌工作且有参与到Android的早期工作】

AsyncTask的问题五:可靠性

简而言之,支持AsyncTask的THREAD_POOL_EXECUTOR默认配置是错误而且不可靠的。谷歌这些年里至少调整了两次它的配置,但它依然使得官方Android设置程序崩溃。 crashed the official Android Settings application

大多数Android应用程序永远不会需要这种层级的并发,然而,你永远不会知道一年之后你会使用什么样的用例,所以,依靠不可靠的解决方案是有问题的。

AsyncTask的问题六:错误的并发概念

这一点与糟糕的文档相关,但我想它值得单独被列出,[Javadoc for executeOnExecutor() method](https://developer.android.com/reference/android/os/AsyncTask.html#executeOnExecutor(java.util.concurrent.Executor, Params…))

声明:

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的默认超时时间是10s。如果你确实使用了SERIAL_EXECUTOR,在任何时候仅执行一个任务,你会停止你应用中所有后台任务10s钟,如果你恰巧发送了两个请求且都超时了,就会有20s没有后台进程。现在网络请求超时不属于任何异常,这对于大多数其他用例也相同:数据库请求,图片加载,计算,IPC,等等。

是的,就像文档中所声明的那样,被剥离出的操作例如线程池的执行顺序因为并发执行没定,但这并不是一个问题,实际上,这就是并发的定义。

所以我想这个官方文档上AsyncTask的作者的关于并发的声明具有很严重的概念上的错误。看不到对官方文档中这种误导性信息的其他任何解释。

AsyncTask的未来

希望我已经说服了你弃用AsyncTask是谷歌走的一步好棋。然而,对于今天正在使用AsyncTask的项目来说,这不是好的消息。如果你的项目是这样的,你需要现在重构你的项目吗?

首先,我不认为你需要积极地从你的代码中移除AsyncTask。弃用这个API并不意味着它停止工作。事实上,你不用惊讶AsyncTask会在Android的生命周期中长时间的存在。对于太多的应用,包括谷歌自己的应用,都在使用这个API。即便它会被,例如说在五年之后,被移除,你依然可以拷贝代码并粘贴到你的项目中并改变引用声明来维持它的运行逻辑。

这个弃用最大的影响是对于新的Android开发者,对于他们来说不需要投入时间来学习和使用AsyncTask是显而易见的了。

Android并发编程的未来

AsyncTask的弃用留下了一些必定会被其它多线程编程方法填补的空白。将会是什么呢?让我跟你分享一下我在这个问题上的观点。

如果你使用Java开始你的Android之旅,我建议直接使用Thread类和UI Handler。很多Android开发者可能会反对,但我自己这样用了有一段时间了而且感觉还不错,比AsyncTask好多了。为了获得一些用这个技术的反馈,我在Twitter上发起了一个投票,直到我正在写这篇文章,结果是这样的:

[
看来我不是唯一尝试使用这个方法并发现还可以的人。

如果你已经竟有了一些经验,你可以使用集中的ExecutorService取代手工的实例化线程。对于我来说最大的问题是使用Thread类时总是忘记启动线程从而需要浪费些时间在这种傻傻的错误上,这很烦人,ExecutorService解决了这个问题。

[顺便说一下,如果您正在阅读本文并想发表有关性能的评论,请确保您的评论中包含实际效果指标]

现在我个人在Java并发编程中更喜欢使用我自己的 ThreadPoster library。这是一个基于ExecutorService和Handler很轻量级的抽象。这个库使得并发更加明确且易于单元测试。

如果你是用Kotlin,上面的建议依然有效,但还有更多需要考虑的因素

看起来协程框架将要成为Kotlin的官方并发开发支持。换句话说,即便Kotlin在Android中依然使用线程作为底层支持,但协程会成为语言文档和教程中的最低级别的抽象。

对于我个人来说,在目前来看协程很复杂而且不够成熟,但是我总是根据两年后对生态系统的预测来选择工具,按照这个标准,kotlin项目中我会选择使用协程。因此,我建议所有使用kotlin的开发者提升并迁移到协程上。

更重要的是,忽略你实用的方法,投入更多的时间学习并发基础。就像你在这篇文章中所看到的,你的并发代码的正确性不不依靠于框架,而是你对底层原则的理解。

结论

我想AsyncTask的弃用是有些姗姗来迟的并且这使得Android生态的并发开发更加清晰。这个API有很多问题并且在过去的几年里造成了不少的问题。

不幸的是,官方的弃用说明信息中包含了错wide信息并且有可能引起今天正在使用AsyncTask的开发者的疑惑,希望这篇文章澄清了有关AsyncTask的一些观点,且还为您提供了关于Android并发性的一般思考。

对于今天正在使用AsyncTask的项目这个弃用可能会造成一些麻烦,但并不需要什么立即的改动,短期内AsyncTask并不会从Android中移除。

另外,如果你想深入的学习Android并发编程,可以我的相关课程my new course about multithreading in Android。其中包含了所有专业的Android开发所需要的并发知识,从硬件原理,到Thread类,到kotlin协程。

oid并发性的一般思考。

对于今天正在使用AsyncTask的项目这个弃用可能会造成一些麻烦,但并不需要什么立即的改动,短期内AsyncTask并不会从Android中移除。

另外,如果你想深入的学习Android并发编程,可以我的相关课程my new course about multithreading in Android。其中包含了所有专业的Android开发所需要的并发知识,从硬件原理,到Thread类,到kotlin协程。

与往常一样,感谢您的阅读,请在下面留下您的评论和问题。

  • 22
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值