震驚!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協程。

與往常一樣,感謝您的閱讀,請在下面留下您的評論和問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章