Android 效能優化(十一)之正確的非同步姿勢

頭條祁同偉發表於2017-05-22

1、 前言

在前面的效能優化系列文章中,我曾多次說過:非同步不是靈丹妙藥,不正確的非同步方式不僅不能較好的完成非同步任務,反而會加劇卡頓。Android開發中我們使用非同步來進行耗時操作,非同步離不開一個詞:執行緒。那麼問題來了:

  1. Android中執行緒排程是如何實現的?
  2. 正確的非同步姿勢是什麼呢?
  3. 執行緒池一定會提升效率嗎?

那今天這篇文章我們就來聊聊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必須以某種方式處理這些問題。

Android 效能優化(十一)之正確的非同步姿勢
Android中執行緒優先順序

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

Android 效能優化(十一)之正確的非同步姿勢
歡迎關注

相關文章