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乾貨!