參考
Android線程的正確使用姿勢
Android性能優(yōu)化典范之多線程篇
Android多線程編程的總結
Android中8種異步處理與計算的方法
概要:Thread(),AsyncTask適合處理單個任務的場景晓勇,HandlerThread適合串行處理多任務的場景。當需要并行的處理多任務之時,ThreadPoolExecutor是更好的選擇,尤其適合處理大量耗時較短的任務,避免出現單個任務阻塞整個隊列的情況。IntentService看做是Service和HandlerThread的結合體直焙,不需要與UI交互奋单,適合需要在工作線程處理UI無關任務的場景。
一具被、線程調度(Thread Scheduling)
Android系統(tǒng)基于精簡過后的linux內核,其線程的調度受時間片輪轉和優(yōu)先級控制等諸多因素影響只损。不少初學者會認為某個線程分配到的time slice多少是按照其優(yōu)先級與其它線程優(yōu)先級對比所決定的一姿,這并不完全正確。
Linux系統(tǒng)的調度器在分配time slice的時候跃惫,采用的CFS(completely fair scheduler)策略叮叹。這種策略不但會參考單個線程的優(yōu)先級,還會追蹤每個線程已經獲取到的time slice數量爆存,如果高優(yōu)先級的線程已經執(zhí)行了很長時間蛉顽,但低優(yōu)先級的線程一直在等待,后續(xù)系統(tǒng)會保證低優(yōu)先級的線程也能獲取更多的CPU時間先较。顯然使用這種調度策略的話携冤,優(yōu)先級高的線程并不一定能在爭取time slice上有絕對的優(yōu)勢,所以Android系統(tǒng)在線程調度上使用了cgroups的概念闲勺,cgroups能更好的凸顯某些線程的重要性曾棕,使得優(yōu)先級更高的線程明確的獲取到更多的time slice。
Android將線程分為多個group菜循,其中兩類group尤其重要翘地。一類是default group,UI線程屬于這一類癌幕。另一類是background group衙耕,工作線程應該歸屬到這一類。background group當中所有的線程加起來總共也只能分配到5~10%的time slice勺远,剩下的全部分配給default group橙喘,這樣設計顯然能保證UI線程繪制UI的流暢性。
有不少人吐槽Android系統(tǒng)之所以不如iOS流暢谚中,是因為UI線程的優(yōu)先級和普通工作線程一致導致的渴杆。這其實是個誤會寥枝,Android的設計者實際上提供了background group的概念來降低工作線程的CPU資源消耗,只不過與iOS不同的是磁奖,Android開發(fā)者需要顯式的將工作線程歸于background group囊拜。
new Thread(new Runnable() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
}).start();
所以在我們決定新啟一個線程執(zhí)行任務的時候,首先要問自己這個任務在完成時間上是否重要到要和UI線程爭奪CPU資源比搭。如果不是冠跷,降低線程優(yōu)先級將其歸于background group,如果是身诺,則需要進一步的profile看這個線程是否造成UI線程的卡頓蜜托。
在 Android 系統(tǒng)里面,我們可以通過 android.os.Process.setThreadPriority(int) 設置線程的優(yōu)先級霉赡,參數范圍從-20到19橄务,數值越小優(yōu)先級越高。Android 系統(tǒng)還為我們提供了以下的一些預設值穴亏,我們可以通過給不同的工作線程設置不同數值的優(yōu)先級來達到更細粒度的控制蜂挪。
Android 系統(tǒng)里面的 AsyncTask 與 IntentService已經默認幫助我們設置線程的優(yōu)先級,但是對于那些非官方提供的多線程工具類嗓化,我們需要特別留意根據需要自己手動來設置線程的優(yōu)先級棠涮。
二、用什么姿勢開線程刺覆?
1.new Thread()
這是Android系統(tǒng)里開線程最簡單的方式严肪,也只能應用于最簡單的場景,簡單的好處卻伴隨不少的隱患谦屑。
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
- 這種方式僅僅是起動了一個新的線程驳糯,沒有任務的概念,不能做狀態(tài)的管理伦仍。start之后结窘,run當中的代碼就一定會執(zhí)行到底,無法中途取消充蓝。
- Runnable作為匿名內部類還持有了外部類的引用隧枫,在線程退出之前,該引用會一直存在谓苟,阻礙外部類對象被GC回收官脓,在一段時間內造成內存泄漏。
- 沒有線程切換的接口涝焙,要傳遞處理結果到UI線程的話卑笨,需要寫額外的線程切換代碼。
- 如果從UI線程啟動仑撞,則該線程優(yōu)先級默認為Default赤兴,歸于default cgroup妖滔,會平等的和UI線程爭奪CPU資源。這一點尤其需要注意桶良,在對UI性能要求高的場景下要記得
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- 雖說處于background group的線程總共只能爭取到5~10%的CPU資源座舍,但這對絕大部分的后臺任務處理都綽綽有余了,1ms和10ms對用戶來說陨帆,都是快到無法感知曲秉,所以我們一般都偏向于在background group當中執(zhí)行工作線程任務。
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回調來嚴格規(guī)范工作線程與UI線程之間的交互承二。我們大部分的業(yè)務場景幾乎都符合這種規(guī)范,比如去磁盤讀取圖片纲爸,縮放處理需要在工作線程執(zhí)行亥鸠,最后繪制到ImageView控件需要切換到UI線程。
- AsyncTask的幾處回調都給了我們機會去中斷任務缩焦,在任務狀態(tài)的管理上較之Thread()方式更為靈活读虏。值得注意的是AsyncTask的cancel()方法并不會終止任務的執(zhí)行,開發(fā)者需要自己在doInBackground中去檢查cancel的狀態(tài)值來決定是否中止任務袁滥。
- AsyncTask也有隱式的持有外部類對象引用的問題,只要 Task 沒有結束灾螃,引用關系就會一直存在题翻,需要特別注意防止出現意外的內存泄漏。
- AsyncTask由于在不同的系統(tǒng)版本上串行與并行的執(zhí)行行為不一致腰鬼,被不少開發(fā)者所詬病嵌赠,這確實是硬傷,絕大部分的多線程場景都需要明確任務是串行還是并行熄赡。具體來講姜挺,在Android1.6前,AsyncTask是串行執(zhí)行任務的彼硫,android1.6時采用線程池并行任務炊豪,但是從Androide3.0開始,為了避免并發(fā)錯誤拧篮,又采用一個線程串行執(zhí)行任務词渤。
- 線程優(yōu)先級為background,對UI線程的執(zhí)行影響極小串绩。
3.HandlerThread
在需要對多任務做更精細控制缺虐,線程切換更頻繁的場景之下,Thread()和AsyncTask都會顯得力不從心礁凡。HandlerThread卻能勝任這些需求甚至更多高氮。
HandlerThread將Handler慧妄,Thread,Looper剪芍,MessageQueue幾個概念相結合腰涧。Handler是線程對外的接口,所有新的message或者runnable都通過handler post到工作線程紊浩。Looper在MessageQueue取到新的任務就切換到工作線程去執(zhí)行窖铡。不同的post方法可以讓我們對任務做精細的控制,什么時候執(zhí)行坊谁,執(zhí)行的順序都可以控制费彼。HandlerThread最大的優(yōu)勢在于引入MessageQueue概念,可以進行多任務隊列管理口芍。
我們需要一個執(zhí)行在工作線程箍铲,同時又能夠處理隊列中的復雜任務的功能,而 HandlerThread 的出現就是為了實現這個功能的鬓椭,它組合了 Handler颠猴,MessageQueue,Looper 實現了一個長時間運行的線程小染,不斷的從隊列中獲取任務進行執(zhí)行的功能翘瓮。HandlerThread 比較合適處理那些在工作線程執(zhí)行,需要花費時間偏長的任務裤翩。我們只需要把任務發(fā)送給 HandlerThread资盅,然后就只需要等待任務執(zhí)行結束的時候通知返回到主線程就好了。
- HandlerThread背后只有一個線程踊赠,所以任務是串行執(zhí)行的呵扛。串行相對于并行來說更安全,各任務之間不會存在多線程安全問題筐带。
- HandlerThread所產生的線程會一直存活今穿,Looper會在該線程中持續(xù)的檢查MessageQueue。這一點和Thread()伦籍,AsyncTask都不同蓝晒,thread實例的重用可以避免線程相關的對象的頻繁重建和銷毀。
- HandlerThread較之Thread()鸽斟,AsyncTask需要寫更多的代碼拔创,但在實用性,靈活度富蓄,安全性上都有更好的表現剩燥。
4.ThreadPoolExecutor
Thread(),AsyncTask適合處理單個任務的場景,HandlerThread適合串行處理多任務的場景。當需要并行的處理多任務之時灭红,ThreadPoolExecutor是更好的選擇,尤其適合處理大量耗時較短的任務侣滩,避免出現單個任務阻塞整個隊列的情況。
例如我們需要一次性 decode 40張圖片变擒,每個線程需要執(zhí)行 4ms 的時間君珠,如果我們使用專屬單線程的方案,所有圖片執(zhí)行完畢會需要花費 160ms(40*4)娇斑,但是如果我們創(chuàng)建10個線程策添,每個線程執(zhí)行4個任務,那么我們就只需要16ms就能夠把所有的圖片處理完畢毫缆。
public static Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
- 線程池可以避免線程的頻繁創(chuàng)建和銷毀唯竹,顯然性能更好,但線程池并發(fā)的特性往往也是疑難雜癥的源頭苦丁,是代碼降級和失控的開始浸颓。多線程并行導致的bug往往是偶現的,不方便調試旺拉,一旦出現就會耗掉大量的開發(fā)精力产上。
- ThreadPool較之HandlerThread在處理多任務上有更高的靈活性,但也帶來了更大的復雜度和不確定性蛾狗。
- 使用線程池需要特別注意同時并發(fā)線程數量的控制晋涣,理論上來說,我們可以設置任意你想要的并發(fā)數量淘太,但是這樣做非常的不好姻僧。因為 CPU 只能同時執(zhí)行固定數量的線程數,一旦同時并發(fā)的線程數量超過 CPU 能夠同時執(zhí)行的閾值蒲牧,CPU 就需要花費精力來判斷到底哪些線程的優(yōu)先級比較高,需要在不同的線程之間進行調度切換赌莺。一旦同時并發(fā)的線程數量達到一定的量級冰抢,這個時候 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 是執(zhí)行在主線程的,可是通常情況下霜大,這很容易影響到程序的繪制性能(搶占了主線程的資源)构哺。除了前面介紹過的 AsyncTask 與 HandlerThread,我們還可以選擇使用 IntentService 來實現異步操作战坤。IntentService 繼承自普通 Service 同時又在內部創(chuàng)建了一個 HandlerThread曙强,在 onHandlerIntent()的回調里面處理扔到 IntentService 的任務。所以 IntentService 就不僅僅具備了異步線程的特性途茫,還同時保留了 Service 不受主頁面生命周期影響的特點碟嘴。
- 因為 IntentService 內置的是 HandlerThread 作為異步線程,所以每一個交給 IntentService 的任務都將以隊列的方式逐個被執(zhí)行到囊卜,一旦隊列中有某個任務執(zhí)行時間過長娜扇,那么就會導致后續(xù)的任務都會被延遲處理。
- 通常使用到 IntentService 的時候栅组,我們會結合使用 BroadcastReceiver 把工作線程的任務執(zhí)行結果返回給主 UI 線程雀瓢。使用廣播容易引起性能問題,我們可以使用 LocalBroadcastManager 來發(fā)送只在程序內部傳遞的廣播玉掸,從而提升廣播的性能刃麸。我們也可以使用 runOnUiThread() 快速回調到主 UI 線程。
- 包含正在運行的 IntentService 的程序相比起純粹的后臺程序更不容易被系統(tǒng)殺死司浪,該程序的優(yōu)先級是介于前臺程序與純后臺程序之間的泊业。
6.Threading and Loaders
參考Loader的初步學習筆記
當啟動工作線程的 Activity 被銷毀的時候,我們應該做點什么呢啊易?為了方便的控制工作線程的啟動與結束吁伺,Android 為我們引入了 Loader 來解決這個問題。我們知道 Activity 有可能因為用戶的主動切換而頻繁的被創(chuàng)建與銷毀租谈,也有可能是因為類似屏幕發(fā)生旋轉等被動原因而銷毀再重建篮奄。在 Activity 不停的創(chuàng)建與銷毀的過程當中,很有可能因為工作線程持有 Activity 的 View 而導致內存泄漏(因為工作線程很可能持有 View 的強引用,另外工作線程的生命周期還無法保證和 Activity 的生命周期一致宦搬,這樣就容易發(fā)生內存泄漏了)牙瓢。除了可能引起內存泄漏之外,在 Activity 被銷毀之后间校,工作線程還繼續(xù)更新視圖是沒有意義的矾克,因為此時視圖已經不在界面上顯示了晰洒。
Loader 的出現就是為了確保工作線程能夠和 Activity 的生命周期保持一致锈嫩,同時避免出現前面提到的問題。
LoaderManager 會對查詢的操作進行緩存导梆,只要對應 Cursor 上的數據源沒有發(fā)生變化滓彰,在配置信息發(fā)生改變的時候(例如屏幕的旋轉)控妻,Loader 可以直接把緩存的數據回調到 onLoadFinished(),從而避免重新查詢數據揭绑。另外系統(tǒng)會在 Loader 不再需要使用到的時候(例如使用 Back 按鈕退出當前頁面)回調 onLoaderReset()方法弓候,我們可以在這里做數據的清除等等操作。
在 Activity 或者 Fragment 中使用 Loader 可以方便的實現異步加載的框架他匪,Loader 有諸多優(yōu)點菇存。但是實現 Loader 的這套代碼還是稍微有點點復雜,Android 官方為我們提供了使用 Loader 的示例代碼進行參考學習邦蜜。
三依鸥、網友評價
謝謝分享,公司用了HandlerThread悼沈,在Activity退出時還要調用Looper的quit方法贱迟,現在他們懷疑系統(tǒng)性能差是由于很多Looper在輪循導致,想廢掉這種方法
之前試過用ThreadPool絮供,發(fā)現設置最大線程數之后會有坑衣吠,有些頁面的線程要等很久才執(zhí)行,因為之前頁面的線程還沒執(zhí)行完壤靶,后來又換回了HandlerThread