1、 前言
在前面的性能優化系列文章中,我曾多次說過:異步不是靈丹妙藥,不正確的異步方式不僅不能較好的完成異步任務,反而會加劇卡頓。Android開發中我們使用異步來進行耗時操作,異步離不開一個詞:線程。那么問題來了:
- Android中線程調度是如何實現的?
- 正確的異步姿勢是什么呢?
- 線程池一定會提升效率嗎?
那今天這篇文章我們就來聊聊Android中正確的異步姿勢。
2、 Android線程調度
Android的線程調度由兩個主要因素來決定如何在整個系統調度線程:nice values和cgroups。
2.1 Nice values
Linux中使用nice value來設定一個進程的優先級,系統任務調度器根據這個值來安排調度。而在Android中nice values被用在線程優先級上,高nice values(低優先級)的線程運行機會少于低nice values(高優先級)的線程。最重要的兩個線程優先級是default和background。線程的優先級應該根據線程的工作量謹慎選擇,簡單來說,線程優先級應該和該線程期望完成的工作量相反。線程做的工作越多,它的優先級應該越小,以便它不會造成系統資源緊張。所以,UI線程(Activity的主線程)通常是default優先級,然而后臺線程(AsyncTask的線程)通常是background優先級。
Nice values在理論上很重要,因為他們減少了后臺工作線程中斷UI的可能性。 但在實踐中,只有Nice values并不足夠。例如,存在20個后臺線程和一個單獨的執行UI的前臺線程。雖然他們每個的優先級很低,但是合起來這個20個后臺線程將影響前臺線程的性能,結果就是損害了用戶體驗。因為在任何時刻幾個應用程序可能已經有等待運行的后臺線程,Android OS必須以某種方式處理這些問題。
2.2 Cgroups
為了處理這個問題,Android系統使用Linux cgroups(Linux內核的一個功能,用來限制,控制與分離一個進程組群的資源)強制執行更嚴格的foreground、background調度策略。background優先級的線程被隱式的移動到了background cgroup,當其它組中的線程處于工作狀態,它們被限制只有很小的幾率(5%到10%)利用CPU。這種分離允許后臺線程執行一些任務,但不會對用戶可見的前臺線程產生較大的影響。
除了自動將低優先級線程分配給background cgroup,Android也將當前不在前臺運行的應用程序的線程移動到background cgroup中。將應用程序線程自動分組保證了當前前臺線程總是優先的,無論有多少應用程序在后臺運行。
總結:
- 高Nice Value對應較低的線程優先級,意味著更少的執行機會,讓步于高優先級的UI線程;
- Cgroups可以更好的凸顯某類線程的優先級,Android中有兩類group尤其重要:一類是default group,對應UI線程。另一類是background group,對應工作線程;
- 進程的屬性變化也會影響到線程的調度,當一個App進入后臺,該App所屬的整個線程都將進入background group,以確保處于foreground、用戶可見的進程能獲取到盡可能多的CPU資源。
3、 正確的異步姿勢
3.1 Thread
new Thread(){
@Override
public void run() {
super.run();
// NetWork or DataBase Operation
}
}.start();
這是最簡單的創建異步線程的姿勢了,但是每當項目中出現這類代碼,我都忍不了要把它改掉的沖動。
缺點:
- 創建及銷毀線程消耗性能較大;
- 缺乏統一的管理;
- 優先級與UI線程一致,搶占資源處于同一起跑線;
- 匿名內部類默認持有外部類的引用,有內存泄漏的風險;
- 需要自己處理線程切換。
備注:此種姿勢最好不要使用,特定場景下(例如App啟動階段為避免在主線程創建線程池的資源消耗)使用的話務必加上優先級的設置。
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
3.2 AysncTask
AsyncTask是Android1.5提供了工具類,它使創建異步任務變得更加簡單,同時屏蔽了線程切換。
下面代碼是官方文檔的示例代碼,在doInBackground()方法中處理耗時操作,處理的進度由onProgressUpdate()方法進行回調,耗時操作處理完成之后會調用onPostExecute()方法,在UI線程中執行。
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
if (isCancelled()) break;
}
return totalSize;
}
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}
protected void onPostExecute(Long result) {
showDialog("Downloaded " + result + " bytes");
}
}
優點:
- 創建異步任務變得更加簡單,同時屏蔽了線程切換;
- 在AsyncTask.java中我們可以看到,異步線程的優先級已經被默認設置成了:THREAD_PRIORITY_BACKGROUND,不會與UI線程搶占資源;
缺點:
- Api實現版本不一致問題:在Android1.5時AsyncTask的執行是串行的,在Android1.5——3.0之間AsyncTask是并行的,而到了Android3.0之后AsyncTask的執行又回歸到了串行。當然目前我們兼容的最低版本一般都會是最低4.0,那么就不需要對其進行過多的自定義適配,但是一定要注意AsyncTask默認是串行的,用于多線程場景下的話需要調用其重載方法executeOnExecutor()傳入自定義的線程池,并且自己處理好同步問題;
- 匿名內部類默認持有外部類的引用,有內存泄漏的風險。
備注:對于AsyncTask正確的使用姿勢,就是區分場景調用不同的執行方法;并且避免出現內存泄漏的問題。
3.3 HandlerThread
通過HandlerThread可以創建一個帶有looper的線程,引入了Handler、Looper、MessageQueue等概念,可以實現對工作線程的調度。
以下是HandlerThread的使用示例:
HandlerThread handlerThread = new HandlerThread("DataBase Opeartion", Process.THREAD_PRIORITY_BACKGROUND);
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper()){
@Override
public void handleMessage(Message msg) {
// Do DataBase Opeartion
}
};
優點:
- 串行執行,沒有并發帶來的問題;
- 不退出的前提下一直存在,避免線程相關的對象頻繁重建和銷毀造成的資源消耗。
缺點:
- 串行執行(不同的視角優點也變缺點),并發場景下無能為力;
- 不指定優先級的情景下默認優先級為THREAD_PRIORITY_DEFAULT,與UI線程同級別。
備注:HandlerThread的正確使用姿勢:串行場景,并在構造方法中明確指定優先級。
3.4 IntentService
根據官方文檔的描述:IntentService是繼承于Service并處理異步請求的一個類,在IntentService內有一個工作線程來處理耗時操作,啟動IntentService的方式和啟動傳統Service一樣,同時,當任務執行完后,IntentService會自動停止,而不需要我們去手動控制。另外,可以啟動IntentService多次,而每一個耗時操作會以工作隊列的方式在IntentService的onHandleIntent回調方法中執行,并且,每次只會執行一個耗時操作,依次執行。
實際上IntentService是Service與HandlerThread的組合,內部的工作線程以及調度機制都依賴于HandlerThread。
@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);
}
@Override
public void onDestroy() {
mServiceLooper.quit();
}
優勢:
- 同HandlerThread的優勢;
- 開啟服務,進程優先級會提升;
- 無需手動關閉,執行完之后自動結束。
備注:
有人可能對于Service的理解會有誤區,Service并不是執行耗時操作的樂園,在《Android 性能優化(七)之你真的理解 ANR 嗎?》中分析過,Service中執行耗時操作會導致ANR。
3.5 ThreadPoolExecutor
線程池:基本思想是一種對象池的思想,開辟一塊內存空間,里面存放了眾多(存活狀態)的線程,池中線程執行調度由池管理器來處理。當有線程任務時,從池中取一個,執行完成后線程對象歸池,這樣可以避免反復創建線程對象所帶來的性能開銷,節省了系統的資源。
優勢:
- 線程的創建和銷毀由線程池維護,一個線程在完成任務后并不會立即銷毀,而是由后續的任務復用這個線程,從而減少線程的創建和銷毀,節約系統的開銷;
- 線程池旨在線程的復用,這就可以節約我們用以往的方式創建線程和銷毀所消耗的時間,減少線程頻繁調度的開銷,從而節約系統資源,提高系統吞吐量;
- 在執行大量異步任務時提高了性能;
- Java內置的一套ExecutorService線程池相關的api,可以更方便的控制線程的最大并發數、線程的定時任務、單線程的順序執行等。
備注:回到我們上面提的第三個問題:線程池一定會提升效率嗎?
- 使用線程池需要特別注意同時并發線程數量的控制。因為CPU只能同時執行固定數量的線程數,一旦同時并發的線程數量超過CPU能夠同時執行的閾值,CPU就需要花費精力來判斷到底哪些線程的優先級比較高,在不同的線程之間進行調度切換。一旦同時并發的線程數量達到一定的量級,CPU在不同線程之間進行調度的時間就可能過長,反而導致性能嚴重下降;
- 每開一個新的線程,都會耗費至少64K以上的內存。線程池中存在了過多的并發數量不僅會影響CPU的調度時間而且會減少可用內存;
- 線程的優先級具有繼承性,在某線程中創建的線程會繼承此線程的優先級。那么我們在UI線程中創建了線程池,其中的線程優先級是和UI線程優先級一樣的;所以仍然可能出現20個同樣優先級的線程平等的和UI線程搶占資源。
對于線程池中線程數量的限制,可以參考AsyncTask中的配置,基于7.0源碼,不同版本的實現可能有細微差別;
// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work 核心池數量被限定在2到4之間。
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
4、 總結
- Thread、AsyncTask適合處理單個任務的場景;
- HandlerThread適合串行處理多任務的場景;
- IntentService適合處理與UI無關的多任務場景;
- 當需要并行的處理多任務之時,ThreadPoolExecutor是更好的選擇,當然也可以使用AsyncTask傳入自定義的線程池;
- 注意線程優先級的設置;
- 特別注意對不同場景下異步方式的選擇。
參考:
《Java線程池》
《Thread Scheduling in Android》
《java線程池大小為何會大多被設置成CPU核心數+1?》
《Android性能優化典范——The Importance of Thread Priority 》
歡迎關注微信公眾號:定期分享Java、Android干貨!