參考
Android線程的正確使用姿勢
Android性能優化典范之多線程篇
Android多線程編程的總結
Android中8種異步處理與計算的方法
概要:Thread(),AsyncTask適合處理單個任務的場景,HandlerThread適合串行處理多任務的場景。當需要并行的處理多任務之時,ThreadPoolExecutor是更好的選擇,尤其適合處理大量耗時較短的任務,避免出現單個任務阻塞整個隊列的情況。IntentService看做是Service和HandlerThread的結合體,不需要與UI交互,適合需要在工作線程處理UI無關任務的場景。
一、線程調度(Thread Scheduling)
Android系統基于精簡過后的linux內核,其線程的調度受時間片輪轉和優先級控制等諸多因素影響。不少初學者會認為某個線程分配到的time slice多少是按照其優先級與其它線程優先級對比所決定的,這并不完全正確。
Linux系統的調度器在分配time slice的時候,采用的CFS(completely fair scheduler)策略。這種策略不但會參考單個線程的優先級,還會追蹤每個線程已經獲取到的time slice數量,如果高優先級的線程已經執行了很長時間,但低優先級的線程一直在等待,后續系統會保證低優先級的線程也能獲取更多的CPU時間。顯然使用這種調度策略的話,優先級高的線程并不一定能在爭取time slice上有絕對的優勢,所以Android系統在線程調度上使用了cgroups的概念,cgroups能更好的凸顯某些線程的重要性,使得優先級更高的線程明確的獲取到更多的time slice。
Android將線程分為多個group,其中兩類group尤其重要。一類是default group,UI線程屬于這一類。另一類是background group,工作線程應該歸屬到這一類。background group當中所有的線程加起來總共也只能分配到5~10%的time slice,剩下的全部分配給default group,這樣設計顯然能保證UI線程繪制UI的流暢性。
有不少人吐槽Android系統之所以不如iOS流暢,是因為UI線程的優先級和普通工作線程一致導致的。這其實是個誤會,Android的設計者實際上提供了background group的概念來降低工作線程的CPU資源消耗,只不過與iOS不同的是,Android開發者需要顯式的將工作線程歸于background group。
new Thread(new Runnable() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
}).start();
所以在我們決定新啟一個線程執行任務的時候,首先要問自己這個任務在完成時間上是否重要到要和UI線程爭奪CPU資源。如果不是,降低線程優先級將其歸于background group,如果是,則需要進一步的profile看這個線程是否造成UI線程的卡頓。
在 Android 系統里面,我們可以通過 android.os.Process.setThreadPriority(int) 設置線程的優先級,參數范圍從-20到19,數值越小優先級越高。Android 系統還為我們提供了以下的一些預設值,我們可以通過給不同的工作線程設置不同數值的優先級來達到更細粒度的控制。
Android 系統里面的 AsyncTask 與 IntentService已經默認幫助我們設置線程的優先級,但是對于那些非官方提供的多線程工具類,我們需要特別留意根據需要自己手動來設置線程的優先級。
二、用什么姿勢開線程?
1.new Thread()
這是Android系統里開線程最簡單的方式,也只能應用于最簡單的場景,簡單的好處卻伴隨不少的隱患。
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
- 這種方式僅僅是起動了一個新的線程,沒有任務的概念,不能做狀態的管理。start之后,run當中的代碼就一定會執行到底,無法中途取消。
- Runnable作為匿名內部類還持有了外部類的引用,在線程退出之前,該引用會一直存在,阻礙外部類對象被GC回收,在一段時間內造成內存泄漏。
- 沒有線程切換的接口,要傳遞處理結果到UI線程的話,需要寫額外的線程切換代碼。
- 如果從UI線程啟動,則該線程優先級默認為Default,歸于default cgroup,會平等的和UI線程爭奪CPU資源。這一點尤其需要注意,在對UI性能要求高的場景下要記得
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- 雖說處于background group的線程總共只能爭取到5~10%的CPU資源,但這對絕大部分的后臺任務處理都綽綽有余了,1ms和10ms對用戶來說,都是快到無法感知,所以我們一般都偏向于在background group當中執行工作線程任務。
2.AsyncTask
一個典型的AsyncTask實現如下:
public class MyAsyncTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
return null;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected void onPostExecute(Object o) {
super.onPostExecute(o);
}
}
和使用Thread()不同的是,多了幾處API回調來嚴格規范工作線程與UI線程之間的交互。我們大部分的業務場景幾乎都符合這種規范,比如去磁盤讀取圖片,縮放處理需要在工作線程執行,最后繪制到ImageView控件需要切換到UI線程。
- AsyncTask的幾處回調都給了我們機會去中斷任務,在任務狀態的管理上較之Thread()方式更為靈活。值得注意的是AsyncTask的cancel()方法并不會終止任務的執行,開發者需要自己在doInBackground中去檢查cancel的狀態值來決定是否中止任務。
- AsyncTask也有隱式的持有外部類對象引用的問題,只要 Task 沒有結束,引用關系就會一直存在,需要特別注意防止出現意外的內存泄漏。
- AsyncTask由于在不同的系統版本上串行與并行的執行行為不一致,被不少開發者所詬病,這確實是硬傷,絕大部分的多線程場景都需要明確任務是串行還是并行。具體來講,在Android1.6前,AsyncTask是串行執行任務的,android1.6時采用線程池并行任務,但是從Androide3.0開始,為了避免并發錯誤,又采用一個線程串行執行任務。
- 線程優先級為background,對UI線程的執行影響極小。
3.HandlerThread
在需要對多任務做更精細控制,線程切換更頻繁的場景之下,Thread()和AsyncTask都會顯得力不從心。HandlerThread卻能勝任這些需求甚至更多。
HandlerThread將Handler,Thread,Looper,MessageQueue幾個概念相結合。Handler是線程對外的接口,所有新的message或者runnable都通過handler post到工作線程。Looper在MessageQueue取到新的任務就切換到工作線程去執行。不同的post方法可以讓我們對任務做精細的控制,什么時候執行,執行的順序都可以控制。HandlerThread最大的優勢在于引入MessageQueue概念,可以進行多任務隊列管理。
我們需要一個執行在工作線程,同時又能夠處理隊列中的復雜任務的功能,而 HandlerThread 的出現就是為了實現這個功能的,它組合了 Handler,MessageQueue,Looper 實現了一個長時間運行的線程,不斷的從隊列中獲取任務進行執行的功能。HandlerThread 比較合適處理那些在工作線程執行,需要花費時間偏長的任務。我們只需要把任務發送給 HandlerThread,然后就只需要等待任務執行結束的時候通知返回到主線程就好了。
- HandlerThread背后只有一個線程,所以任務是串行執行的。串行相對于并行來說更安全,各任務之間不會存在多線程安全問題。
- HandlerThread所產生的線程會一直存活,Looper會在該線程中持續的檢查MessageQueue。這一點和Thread(),AsyncTask都不同,thread實例的重用可以避免線程相關的對象的頻繁重建和銷毀。
- HandlerThread較之Thread(),AsyncTask需要寫更多的代碼,但在實用性,靈活度,安全性上都有更好的表現。
4.ThreadPoolExecutor
Thread(),AsyncTask適合處理單個任務的場景,HandlerThread適合串行處理多任務的場景。當需要并行的處理多任務之時,ThreadPoolExecutor是更好的選擇,尤其適合處理大量耗時較短的任務,避免出現單個任務阻塞整個隊列的情況。
例如我們需要一次性 decode 40張圖片,每個線程需要執行 4ms 的時間,如果我們使用專屬單線程的方案,所有圖片執行完畢會需要花費 160ms(40*4),但是如果我們創建10個線程,每個線程執行4個任務,那么我們就只需要16ms就能夠把所有的圖片處理完畢。
public static Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
- 線程池可以避免線程的頻繁創建和銷毀,顯然性能更好,但線程池并發的特性往往也是疑難雜癥的源頭,是代碼降級和失控的開始。多線程并行導致的bug往往是偶現的,不方便調試,一旦出現就會耗掉大量的開發精力。
- ThreadPool較之HandlerThread在處理多任務上有更高的靈活性,但也帶來了更大的復雜度和不確定性。
- 使用線程池需要特別注意同時并發線程數量的控制,理論上來說,我們可以設置任意你想要的并發數量,但是這樣做非常的不好。因為 CPU 只能同時執行固定數量的線程數,一旦同時并發的線程數量超過 CPU 能夠同時執行的閾值,CPU 就需要花費精力來判斷到底哪些線程的優先級比較高,需要在不同的線程之間進行調度切換。一旦同時并發的線程數量達到一定的量級,這個時候 CPU 在不同線程之間進行調度的時間就可能過長,反而導致性能嚴重下降。另外需要關注的一點是,每開一個新的線程,都會耗費至少 64K+ 的內存。
- Runtime.getRuntime().availableProcesser()方法并不可靠,他返回的值并不是真實的 CPU 核心數,因為 CPU 會在某些情況下選擇對部分核心進行睡眠處理,在這種情況下,返回的數量就只能是激活的 CPU 核心數。
5.IntentService
不得不說Android在API設計上粒度很細,同一樣工作可以通過各種不同的類來完成。IntentService又是另一種開工作線程的方式,從名字就可以看出這個工作線程會帶有service的屬性。和AsyncTask不同,沒有和UI線程的交互,也不像HandlerThread的工作線程會一直存活。IntentService背后其實也有一個HandlerThread來串行的處理Message Queue,從IntentService的onCreate方法可以看出:
@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
只不過在所有的Message處理完畢之后,工作線程會自動結束。所以可以把IntentService看做是Service和HandlerThread的結合體,適合需要在工作線程處理UI無關任務的場景。
- 默認的 Service 是執行在主線程的,可是通常情況下,這很容易影響到程序的繪制性能(搶占了主線程的資源)。除了前面介紹過的 AsyncTask 與 HandlerThread,我們還可以選擇使用 IntentService 來實現異步操作。IntentService 繼承自普通 Service 同時又在內部創建了一個 HandlerThread,在 onHandlerIntent()的回調里面處理扔到 IntentService 的任務。所以 IntentService 就不僅僅具備了異步線程的特性,還同時保留了 Service 不受主頁面生命周期影響的特點。
- 因為 IntentService 內置的是 HandlerThread 作為異步線程,所以每一個交給 IntentService 的任務都將以隊列的方式逐個被執行到,一旦隊列中有某個任務執行時間過長,那么就會導致后續的任務都會被延遲處理。
- 通常使用到 IntentService 的時候,我們會結合使用 BroadcastReceiver 把工作線程的任務執行結果返回給主 UI 線程。使用廣播容易引起性能問題,我們可以使用 LocalBroadcastManager 來發送只在程序內部傳遞的廣播,從而提升廣播的性能。我們也可以使用 runOnUiThread() 快速回調到主 UI 線程。
- 包含正在運行的 IntentService 的程序相比起純粹的后臺程序更不容易被系統殺死,該程序的優先級是介于前臺程序與純后臺程序之間的。
6.Threading and Loaders
參考Loader的初步學習筆記
當啟動工作線程的 Activity 被銷毀的時候,我們應該做點什么呢?為了方便的控制工作線程的啟動與結束,Android 為我們引入了 Loader 來解決這個問題。我們知道 Activity 有可能因為用戶的主動切換而頻繁的被創建與銷毀,也有可能是因為類似屏幕發生旋轉等被動原因而銷毀再重建。在 Activity 不停的創建與銷毀的過程當中,很有可能因為工作線程持有 Activity 的 View 而導致內存泄漏(因為工作線程很可能持有 View 的強引用,另外工作線程的生命周期還無法保證和 Activity 的生命周期一致,這樣就容易發生內存泄漏了)。除了可能引起內存泄漏之外,在 Activity 被銷毀之后,工作線程還繼續更新視圖是沒有意義的,因為此時視圖已經不在界面上顯示了。
Loader 的出現就是為了確保工作線程能夠和 Activity 的生命周期保持一致,同時避免出現前面提到的問題。
LoaderManager 會對查詢的操作進行緩存,只要對應 Cursor 上的數據源沒有發生變化,在配置信息發生改變的時候(例如屏幕的旋轉),Loader 可以直接把緩存的數據回調到 onLoadFinished(),從而避免重新查詢數據。另外系統會在 Loader 不再需要使用到的時候(例如使用 Back 按鈕退出當前頁面)回調 onLoaderReset()方法,我們可以在這里做數據的清除等等操作。
在 Activity 或者 Fragment 中使用 Loader 可以方便的實現異步加載的框架,Loader 有諸多優點。但是實現 Loader 的這套代碼還是稍微有點點復雜,Android 官方為我們提供了使用 Loader 的示例代碼進行參考學習。
三、網友評價
謝謝分享,公司用了HandlerThread,在Activity退出時還要調用Looper的quit方法,現在他們懷疑系統性能差是由于很多Looper在輪循導致,想廢掉這種方法
之前試過用ThreadPool,發現設置最大線程數之后會有坑,有些頁面的線程要等很久才執行,因為之前頁面的線程還沒執行完,后來又換回了HandlerThread