AsyncTask is Deprecated, Now What?

For the past decade, AysncTask has been one of the most widely used solutions for writing concurrent code in Android. However, it earned very controversial reputation. On the one hand, AsyncTask powered, and still powers, many Android applications. On the other hand, most professional Android developers openly dislike this API.

All in all, I’d say that Android community has love-hate relationship with AsyncTask. But there are big news: the era of AsyncTask is about to end because a commit that deprecated it had just landed in Android Open Source Project.

In this post I’ll review the official statement motivating AsyncTask’s deprecation, as well as the real reasons why it had to be deprecated. As you’ll see, these are different sets of reasons. In addition, towards the end of this article, I’ll share my thoughts on the future of Android’s concurrency APIs.

Official Reason for Deprecation of AsyncTask

The official deprecation of AsyncTask, as well as the motivation for that decision, were introduced with this commit. The newly added first paragraph of Javadoc states:

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.

While that’s the official statement by Google, there are several inaccuracies there which are worth pointing out.

Fist of all, AsyncTask has never been intended to “enable proper and easy use of the UI thread”. It was intended to offload long-running operations from UI thread to background threads, and then deliver the results of these operations back to UI thread. I know, I’m nitpicking here. However, in my opinion, when Google deprecates API that they themselves invented and promoted for years, it would be more respectful towards developers who use this API today, and will continue to use for years to come, to invest more effort into deprecation message to prevent further confusion.

That said, the more interesting part of this deprecation message is this: “that would cause Context leaks, missed callbacks, or crashes on configuration changes”. So, Google basically states that the most common use case for AsyncTask automatically results in very serious problems. However, there are many high-quality applications out there which use AsyncTask and work flawlessly. Even some classes inside AOSP itself use AsyncTask. How comes they don’t experience these problems?

To answer this question, let’s discuss the relationship between AsyncTask and memory leaks in details.

AsyncTask and Memory Leaks

This AsyncTask leaks the enclosing Fragment (or Activity) object forever:

Looks like this example proves Google’s point: AsyncTask indeed causes memory leaks. We should use some other approach to write concurrent code! Well, let’s give it a try.

This is the same example, rewritten using RxJava:

It leaks the enclosing Fragment (or Activity) object too.

Maybe new and shiny Kotlin Coroutines will help? That’s how I’d achieve the same functionality using Coroutines:

Unfortunately, it results in the same exact memory leak.

Looks like this feature leaks the enclosing Fragment (or Activity), regardless of the choice of multithreading framework. In fact, it would result in a leak even if I’d use bare Thread class:

So, it’s not about AsyncTask, after all, but about the logic that I write. To drive this point home, let’s modify the example that uses AsyncTask to fix the memory leak:

In this case, I use the dreadful AsyncTask, but there are no leaks. It’s magic!

Well, of course it’s not magic. It’s just a reflection of the fact that you can write safe and correct multithreaded code using AsyncTask, just like you can do it with any other multithreading framework. There is no special relationship between AsyncTask and memory leaks. Therefore, the common belief that AsyncTask automatically leads to memory leaks, as well as the new deprecation message in AOSP, are simply incorrect.

Edit: the original version of this article used local counter variable, instead of it being a member of the enclosing Activity or Fragment. As several readers correctly pointed out, as opposed to inner classes, lambdas do not capture references to parent objects if they don’t have to. Therefore, if counter variable would be local, the above examples that use lambdas wouldn’t, in fact, leak the enclosing Activity or Fragment. I changed the examples to satisfy that condition. Note, however, that since AsyncTask had been used in Android long before we could use lambdas, the fact that lambdas change the relationship isn’t that important. In addition, I would recommed to avoid relying on this property of lambdas as a mean to avoid memory leaks because otherwise you’ll always be just a small code modification away from it.

You might be wondering now: if this idea that AsyncTask leads to memory leaks is incorrect, why is it so widespread among Android developers?

Well, there is a built in Lint rule in Android Studio which warns you and recommends making your AsyncTasks static to avoid memory leaks. This warning and recommendation are incorrect too, but developers who use AsyncTask in their project get this warning and, since it comes from Google, take it at face value.

In my opinion, the above Lint warning is the reason why the myth that ties AsyncTask to memory leaks is so widespread – it’s being forced on developers by Google itself.

How to Avoid Memory Leaks in Multithreaded Code

Until now we established that there is no automatic causal relationship between usage of AsyncTask and memory leaks. In addition, you saw that memory leaks can happen with any multithreading framework. Consequently, you might be wondering now how to avoid memory leaks in your own applications.

I won’t answer this question in details, because I want to stay on-topic, but I don’t want to leave you empty-handed either. Therefore, allow me to list the concepts that you need to understand to write memory leaks free multithreaded code in Java and Kotlin:

  • Garbage collector
  • Garbage collection roots
  • Thread lifecycle in respect to garbage collection
  • Implicit references from inner classes to parent objects

If you understand these concepts in details, you’ll lower the probability of having memory leaks in your code considerably. On the other hand, if you don’t understand these concepts and you write concurrent code, then it’s just a matter of time before you introduce memory leaks, whatever multithreading framework you’ll use.

Edit:

Since this is basic and important knowledge for all Android developers, I decided to upload the first part of my Android Multithreading Masterclass course to YouTube. It covers the aforementioned topics in much more details than the official documentation. You can watch it here for free.

Was AsyncTask Deprecated For No Reason

Since AsyncTask doesn’t automatically lead to memory leaks, looks like Google deprecated it by mistake, for no reason. Well, not exactly.

For the past years, AsyncTask has already been “effectively deprecated” by Android developers themselves. Many of us openly advocated against usign this API in applications. I, personally, feel sorry for developers who maintain codebases that use AsyncTask extensively. It had been like that for years, and it has been evident that AsyncTask is very problematic API. Therefore, AsyncTask’s deprecation is only logical. If you ask me, Google should’ve deprecated it long ago.

Therefore, while Google is still confused about their own creation, the deprecation itself is very much appropriate and welcome. At the very least, it will let new Android developers know that they don’t need to invest time into learning this API and they won’t use it in their applications.

Said all that, you probably still don’t understand why exaclty AsyncTask was “bad” and why so many developers hated it so much. In my opinion, it’s very interesting and practically useful question. After all, if we don’t understand what exactly were AsyncTask’s issues, there is no guarantee that we will not repeat the same mistakes again.

Therefore, let’s me list what I personally see as the real AsyncTask’s deficiencies.

AsyncTask Problem 1: Makes Multithreading More Complex

One of the main “selling” points of AsyncTask has always been the promise that you won’t need to deal with Thread class and other multithreading primitives yourself. It should’ve made multithreading simpler, especially for new Android developers. Sounds great, right? However, in practice, this “simplicity” backfired manyfold.

AsyncTask’s class-level Javadoc uses the word “thread” 16 times. You simply can’t understand it if you don’t understand what’s thread. In addition, this Javadoc states a bunch of AsyncTask-specific constraints and conditions. In other words, if you want to use AsyncTask, you need to understand threads and you also need to understand the many nuances of AsyncTask itself. It’s not “simpler” by any stretch of imagination.

Furthermore, multithreading is intrinsically complex topic. In my opninion, it’s one of the most complex topics in software in general (and, for that matter, hardware too). Now, unlike many other concepts, you can’t take shortcuts in multithreading because even the smallest mistake can lead to very serious bugs which will be extremely difficult to investigate. There are applications out there which has been affected by multithreading bugs for months even after developers had known that they exist. They just couldn’t find these bugs.

Therefore, in my opinion, there is simply no way to simplify concurrency and AsyncTask’s ambition was destined to fail from the very onset.

AsyncTask Problem 2: Bad Documentation

It’s not a secret that Android documentation is non-optimal (trying to be polite here). It improved over the years, but even today I wouldn’t call it decent. In my opinion, unfortunate documentation was the leading factor to determine AsyncTask’s troubled history. If AsyncTask would be just over-engineered, complex and nuanced multithreading framework, as it is, but with good documentation, it could remain part of the ecosystem. After all, there is no shortage of ugly APIs that Android developers got accustomed to. But AsyncTask’s documentation was terrible and made all its other deficiencies worse.

The worst were the examples. They demonstrated the most unfortunate approaches to write multithreading code: all code inside Activities, complete disregard of lifecycles, no discussion of cancellation scenarios, etc.. If you’d use these examples as they are in your own applications, memory leaks and incorrect behavior would be pretty much guaranteed.

In addition, AsyncTask’s documentation didn’t contain any explanation of the core concepts related to multithreading (the ones I listed earlier, and others). In fact, I think no part of the official documentation did. Not even just a link to JLS Chapter 17: Threads and Locks to refer developers who really wanted to understand concurrency to the “official” reference (the quotes are needed because Oracle documentation isn’t official for Android).

By the way, in my opinion, the aforementioned Lint rule in Android Studio, the one that spread the myth about memory leaks, is also part of the documentation. Therefore, not only the docs were insufficient, but they also contained incorrect information.

AsyncTask Problem 3: Excessive Complexity

AsyncTask has three generic arguments. THREE! If I’m not mistaken, I’ve never seen any other class which required this many generics.

I still remember my first encounters with AsyncTask. By that time I already knew a bit about Java threads and couldn’t understand why multithreading in Android is so difficult. Three generic arguments were very difficult to understand and made me very nervous. In addition, since AsyncTask’s methods are called on different threads, I had to constantly remind myself about that, and then verify that I got it right by reading the docs.

Today, when I know much more about concurrency and Android’s UI thread, I can probably reverse-engineer this information. However, this level of understanding came to me much later in my career, after I had already ditched AsyncTask completely.

And with all these complications, you still must call execute() from UI thread only!

AsyncTask Problem 4: Inheritance Abuse

The philosophy of AsyncTask is grounded in inheritance: whenever you need to execute a task in background, you extend AsyncTask.

Combined with bad documentation, inheritance philosophy pushed developers in the direction of writing huge classes which coupled multithreading, domain and UI logic together in the most inefficient and hard to maintain manner. After all, why not? That’s what the API of AsyncTask lends itself to.

“Favor composition over inheritance” rule from Effective Java, if followed, would make a real difference in case of AsyncTask. [Interestingly, Joshua Bloch, the author of Effective Java, worked at Google and was involved in Android at a relatively early stage]

AsyncTask Problem 5: Reliability

Simply put, default THREAD_POOL_EXECUTOR that backs AsyncTask is misconfigured and unreliable. Google tweaked its configuration at least twice over the years (this commit and this one), but it still crashed the official Android Settings application.

Most Android applications will never need this level of concurrency. However, you never know what will be your use cases a year from now, so relying on non-reliable solution is problematic.

AsyncTask Problem 6: Concurrency Misconception

This point is related to bad documentation, but I think it deserves a bullet point on its own. Javadoc for executeOnExecutor() method states:

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

Well, that’s just wrong. Allowing multiple tasks to run concurrently is exaclty what you want in most cases when offloading work from UI thread.

For example, let’s say you send a network request and it times out for whatever reason. The default timeout in OkHttp is 10 seconds. If you indeed use SERIAL_EXECUTOR, which executes just one single task at any given instant, you’ve just stopped all background work in your app for 10 seconds. And if you happen to send two requests and both time out? Well, 20 seconds of no background processing. Now, timing out network requests are not any kind of exception, it’s the same for almost any other use case: database queries, image processing, computations, IPC, etc.

Yes, as stated in the documentation, the order of operations offloaded to, for example, thread pool, is not defined because they run concurrently. However, it’s not a problem. In fact, it’s pretty much the definition of concurrency.

Therefore, in my opinion, this statement in the official docs point to some very serious misconception about concurrency among AsyncTask’s authors. I just can’t see any other explanation to such a misleading information found in the official documentation.

The Future of AsyncTask

Hopefully I convinced you that deprecation of AsyncTask is a good move on Google’s part. However, for projects that use AsyncTask today, these aren’t good news. If you work on such a project, should you refactor your code now?

First of all, I don’t think that you need to actively remove AsyncTasks from your code. Deprecation of this API doesn’t mean that it’ll stop working. In fact, I won’t be surprised if AsyncTask will stick around for as long as Android lives. Too many applications, including Google’s own apps, use this API. And even if it’ll be removed in, say, 5 years, you’ll be able to copy paste its code into your own project and change the import statements to keep the logic working.

The main impact of this deprecation will be on new Android developers. It’ll be clear to them that they don’t need to invest time into learning AsyncTask and won’t use it in new applications.

The Future of Multithreading in Android

AsyncTask’s deprecation leaves a bit of a void that must be filled by some other multithreading approach. What should it be? Let me share with you my personal opinion on this subject.

If you just start your Android journey and use Java, I recommend the combo of bare Thread class and UI Handler. Many Android developers will object, but I myself used this approach for a while and it worked much, much better than AsyncTask ever could. To gather a bit more feedback on this technique, I created this Twitter poll. At the time of writing, these were the results:

Looks like I’m not the only one and most developers who tried this approach found it alright.

If you already got a bit of experience, you can replace manual instantiation of Threads with a centralized ExecutorService. For me, the biggest problem with using Thread class was that I constantly forgot to start threads and then had to spend time debugging these silly mistakes. It’s annoying. ExecutorService solved this issue.

[By the way, if you’re reading this and want to leave a comment concerning performance, please make sure that your comment includes actual performance metrics.]

Now, I, personally, prefer to use my own ThreadPoster library for multithreading in Java. It’s very light abstraction over ExecutorService and Handler. This library makes multithreading more explicit and unit testing easier.

If you use Kotlin, then the above recommendations still valid, but there is one more consideration to take into account.

It looks like Coroutines framework is going to be the official Kotlin’s concurrency primitive. In other words, even though Kotlin in Android uses threads under the hood, Coroutines are going to be the minimal level of abstraction in language’s documentation and tutorials.

To me, personally, Coroutines feel very complicated and immature at this point, but I always try to choose tools based on my predictions for the ecosystem two years from now. By this criteria, I’d choose coroutines for a new Kotlin project. Therefore, I recommend all developers who use Kotlin to ramp up and migrate to Coroutines.

Most importantly, regardless of which approach you choose, invest time into learning multithreading fundamentals. As you saw in this post, the correctness of your concurrent code isn’t determined by frameworks, but by your understanding of the underlying principles.

Conclusion

In my opinion, deprecation of AsyncTask was long overdue and it cleared multithreading landscape in Android ecosystem. This API had many issues and had caused much trouble over the years.

Unfortunately, the official deprecation statement contains incorrect information and can potentially confuse developers who use AsyncTask today, or will inherit codebases with AsyncTask in the future. Hopefully, this post clarified some points about AsyncTask specifically, and also gave you food for thought about concurrency in Android in general.

For projects that use AsyncTask today this deprecation is problematic, but doesn’t require any immediate action. AsyncTask won’t be removed from Android any time soon.

By the way, if you want to learn multithreading in Android in depth, check out my new course about multithreading in Android. It covers everything professional Android developers need to know about concurrency: from hardware origins, through Thread class, to Kotlin Coroutines.

As usual, thank you for reading and please leave your comments and questions below.

参考链接


AsyncTask is Deprecated, Now What?

发布者

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注