Android性能優(yōu)化(十一)之正確的異步姿勢

1、 前言

在前面的性能優(yōu)化系列文章中,我曾多次說過:異步不是靈丹妙藥,不正確的異步方式不僅不能較好的完成異步任務(wù),反而會加劇卡頓。Android開發(fā)中我們使用異步來進行耗時操作,異步離不開一個詞:線程。那么問題來了:

  1. Android中線程調(diào)度是如何實現(xiàn)的?
  2. 正確的異步姿勢是什么呢?
  3. 線程池一定會提升效率嗎?

那今天這篇文章我們就來聊聊Android中正確的異步姿勢。

2、 Android線程調(diào)度

Android的線程調(diào)度由兩個主要因素來決定如何在整個系統(tǒng)調(diào)度線程:nice values和cgroups。

2.1 Nice values

Linux中使用nice value來設(shè)定一個進程的優(yōu)先級,系統(tǒng)任務(wù)調(diào)度器根據(jù)這個值來安排調(diào)度。而在Android中nice values被用在線程優(yōu)先級上,高nice values(低優(yōu)先級)的線程運行機會少于低nice values(高優(yōu)先級)的線程。最重要的兩個線程優(yōu)先級是default和background。線程的優(yōu)先級應(yīng)該根據(jù)線程的工作量謹慎選擇,簡單來說,線程優(yōu)先級應(yīng)該和該線程期望完成的工作量相反。線程做的工作越多,它的優(yōu)先級應(yīng)該越小,以便它不會造成系統(tǒng)資源緊張。所以,UI線程(Activity的主線程)通常是default優(yōu)先級,然而后臺線程(AsyncTask的線程)通常是background優(yōu)先級。

Nice values在理論上很重要,因為他們減少了后臺工作線程中斷UI的可能性。 但在實踐中,只有Nice values并不足夠。例如,存在20個后臺線程和一個單獨的執(zhí)行UI的前臺線程。雖然他們每個的優(yōu)先級很低,但是合起來這個20個后臺線程將影響前臺線程的性能,結(jié)果就是損害了用戶體驗。因為在任何時刻幾個應(yīng)用程序可能已經(jīng)有等待運行的后臺線程,Android OS必須以某種方式處理這些問題。

Android中線程優(yōu)先級

2.2 Cgroups

為了處理這個問題,Android系統(tǒng)使用Linux cgroups(Linux內(nèi)核的一個功能,用來限制,控制與分離一個進程組群的資源)強制執(zhí)行更嚴格的foreground、background調(diào)度策略。background優(yōu)先級的線程被隱式的移動到了background cgroup,當其它組中的線程處于工作狀態(tài),它們被限制只有很小的幾率(5%到10%)利用CPU。這種分離允許后臺線程執(zhí)行一些任務(wù),但不會對用戶可見的前臺線程產(chǎn)生較大的影響。

除了自動將低優(yōu)先級線程分配給background cgroup,Android也將當前不在前臺運行的應(yīng)用程序的線程移動到background cgroup中。將應(yīng)用程序線程自動分組保證了當前前臺線程總是優(yōu)先的,無論有多少應(yīng)用程序在后臺運行。

總結(jié):

  • 高Nice Value對應(yīng)較低的線程優(yōu)先級,意味著更少的執(zhí)行機會,讓步于高優(yōu)先級的UI線程;
  • Cgroups可以更好的凸顯某類線程的優(yōu)先級,Android中有兩類group尤其重要:一類是default group,對應(yīng)UI線程。另一類是background group,對應(yīng)工作線程;
  • 進程的屬性變化也會影響到線程的調(diào)度,當一個App進入后臺,該App所屬的整個線程都將進入background group,以確保處于foreground、用戶可見的進程能獲取到盡可能多的CPU資源。

3、 正確的異步姿勢

3.1 Thread

new Thread(){
    @Override
    public void run() {
        super.run();
        // NetWork or DataBase Operation
    }
}.start();

這是最簡單的創(chuàng)建異步線程的姿勢了,但是每當項目中出現(xiàn)這類代碼,我都忍不了要把它改掉的沖動。

缺點:

  • 創(chuàng)建及銷毀線程消耗性能較大;
  • 缺乏統(tǒng)一的管理;
  • 優(yōu)先級與UI線程一致,搶占資源處于同一起跑線;
  • 匿名內(nèi)部類默認持有外部類的引用,有內(nèi)存泄漏的風險;
  • 需要自己處理線程切換。

備注:此種姿勢最好不要使用,特定場景下(例如App啟動階段為避免在主線程創(chuàng)建線程池的資源消耗)使用的話務(wù)必加上優(yōu)先級的設(shè)置。

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

3.2 AysncTask

AsyncTask是Android1.5提供了工具類,它使創(chuàng)建異步任務(wù)變得更加簡單,同時屏蔽了線程切換。

下面代碼是官方文檔的示例代碼,在doInBackground()方法中處理耗時操作,處理的進度由onProgressUpdate()方法進行回調(diào),耗時操作處理完成之后會調(diào)用onPostExecute()方法,在UI線程中執(zhí)行。

 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");
     }
 }

優(yōu)點:

  • 創(chuàng)建異步任務(wù)變得更加簡單,同時屏蔽了線程切換;
  • AsyncTask.java中我們可以看到,異步線程的優(yōu)先級已經(jīng)被默認設(shè)置成了:THREAD_PRIORITY_BACKGROUND,不會與UI線程搶占資源;

缺點:

  • Api實現(xiàn)版本不一致問題:在Android1.5時AsyncTask的執(zhí)行是串行的,在Android1.5——3.0之間AsyncTask是并行的,而到了Android3.0之后AsyncTask的執(zhí)行又回歸到了串行。當然目前我們兼容的最低版本一般都會是最低4.0,那么就不需要對其進行過多的自定義適配,但是一定要注意AsyncTask默認是串行的,用于多線程場景下的話需要調(diào)用其重載方法executeOnExecutor()傳入自定義的線程池,并且自己處理好同步問題;
  • 匿名內(nèi)部類默認持有外部類的引用,有內(nèi)存泄漏的風險。

備注:對于AsyncTask正確的使用姿勢,就是區(qū)分場景調(diào)用不同的執(zhí)行方法;并且避免出現(xiàn)內(nèi)存泄漏的問題。

3.3 HandlerThread

通過HandlerThread可以創(chuàng)建一個帶有l(wèi)ooper的線程,引入了Handler、Looper、MessageQueue等概念,可以實現(xiàn)對工作線程的調(diào)度。

以下是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
    }
};

優(yōu)點:

  • 串行執(zhí)行,沒有并發(fā)帶來的問題;
  • 不退出的前提下一直存在,避免線程相關(guān)的對象頻繁重建和銷毀造成的資源消耗。

缺點:

  • 串行執(zhí)行(不同的視角優(yōu)點也變?nèi)秉c),并發(fā)場景下無能為力;
  • 不指定優(yōu)先級的情景下默認優(yōu)先級為THREAD_PRIORITY_DEFAULT,與UI線程同級別。

備注:HandlerThread的正確使用姿勢:串行場景,并在構(gòu)造方法中明確指定優(yōu)先級。

3.4 IntentService

根據(jù)官方文檔的描述:IntentService是繼承于Service并處理異步請求的一個類,在IntentService內(nèi)有一個工作線程來處理耗時操作,啟動IntentService的方式和啟動傳統(tǒng)Service一樣,同時,當任務(wù)執(zhí)行完后,IntentService會自動停止,而不需要我們?nèi)ナ謩涌刂?。另外,可以啟動IntentService多次,而每一個耗時操作會以工作隊列的方式在IntentService的onHandleIntent回調(diào)方法中執(zhí)行,并且,每次只會執(zhí)行一個耗時操作,依次執(zhí)行。

實際上IntentService是Service與HandlerThread的組合,內(nèi)部的工作線程以及調(diào)度機制都依賴于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();
    }

優(yōu)勢:

  • 同HandlerThread的優(yōu)勢;
  • 開啟服務(wù),進程優(yōu)先級會提升;
  • 無需手動關(guān)閉,執(zhí)行完之后自動結(jié)束。

備注:
有人可能對于Service的理解會有誤區(qū),Service并不是執(zhí)行耗時操作的樂園,在《Android 性能優(yōu)化(七)之你真的理解 ANR 嗎?》中分析過,Service中執(zhí)行耗時操作會導(dǎo)致ANR。

3.5 ThreadPoolExecutor

線程池:基本思想是一種對象池的思想,開辟一塊內(nèi)存空間,里面存放了眾多(存活狀態(tài))的線程,池中線程執(zhí)行調(diào)度由池管理器來處理。當有線程任務(wù)時,從池中取一個,執(zhí)行完成后線程對象歸池,這樣可以避免反復(fù)創(chuàng)建線程對象所帶來的性能開銷,節(jié)省了系統(tǒng)的資源。

優(yōu)勢:

  • 線程的創(chuàng)建和銷毀由線程池維護,一個線程在完成任務(wù)后并不會立即銷毀,而是由后續(xù)的任務(wù)復(fù)用這個線程,從而減少線程的創(chuàng)建和銷毀,節(jié)約系統(tǒng)的開銷;
  • 線程池旨在線程的復(fù)用,這就可以節(jié)約我們用以往的方式創(chuàng)建線程和銷毀所消耗的時間,減少線程頻繁調(diào)度的開銷,從而節(jié)約系統(tǒng)資源,提高系統(tǒng)吞吐量;
  • 在執(zhí)行大量異步任務(wù)時提高了性能;
  • Java內(nèi)置的一套ExecutorService線程池相關(guān)的api,可以更方便的控制線程的最大并發(fā)數(shù)、線程的定時任務(wù)、單線程的順序執(zhí)行等。

備注:回到我們上面提的第三個問題:線程池一定會提升效率嗎?

  • 使用線程池需要特別注意同時并發(fā)線程數(shù)量的控制。因為CPU只能同時執(zhí)行固定數(shù)量的線程數(shù),一旦同時并發(fā)的線程數(shù)量超過CPU能夠同時執(zhí)行的閾值,CPU就需要花費精力來判斷到底哪些線程的優(yōu)先級比較高,在不同的線程之間進行調(diào)度切換。一旦同時并發(fā)的線程數(shù)量達到一定的量級,CPU在不同線程之間進行調(diào)度的時間就可能過長,反而導(dǎo)致性能嚴重下降;
  • 每開一個新的線程,都會耗費至少64K以上的內(nèi)存。線程池中存在了過多的并發(fā)數(shù)量不僅會影響CPU的調(diào)度時間而且會減少可用內(nèi)存;
  • 線程的優(yōu)先級具有繼承性,在某線程中創(chuàng)建的線程會繼承此線程的優(yōu)先級。那么我們在UI線程中創(chuàng)建了線程池,其中的線程優(yōu)先級是和UI線程優(yōu)先級一樣的;所以仍然可能出現(xiàn)20個同樣優(yōu)先級的線程平等的和UI線程搶占資源。

對于線程池中線程數(shù)量的限制,可以參考AsyncTask中的配置,基于7.0源碼,不同版本的實現(xiàn)可能有細微差別;

    // 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 核心池數(shù)量被限定在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、 總結(jié)

  • Thread、AsyncTask適合處理單個任務(wù)的場景;
  • HandlerThread適合串行處理多任務(wù)的場景;
  • IntentService適合處理與UI無關(guān)的多任務(wù)場景;
  • 當需要并行的處理多任務(wù)之時,ThreadPoolExecutor是更好的選擇,當然也可以使用AsyncTask傳入自定義的線程池;
  • 注意線程優(yōu)先級的設(shè)置;
  • 特別注意對不同場景下異步方式的選擇。

參考:
《Java線程池》
《Thread Scheduling in Android》
《java線程池大小為何會大多被設(shè)置成CPU核心數(shù)+1?》
《Android性能優(yōu)化典范——The Importance of Thread Priority 》

歡迎關(guān)注微信公眾號:定期分享Java、Android干貨!

歡迎關(guān)注
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 線程是程序員進階的一道重要門檻。對于移動開發(fā)者來說,“將耗時的任務(wù)放到子線程去執(zhí)行,以保證UI線程的流暢性”是線程...
    vb12閱讀 1,476評論 0 2
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,716評論 25 709
  • Android中的線程 線程,在Android中是非常重要的,主線程處理UI界面,子線程處理耗時操作。如果在主線程...
    shenhuniurou閱讀 875評論 0 3
  • 系列文章Android面試攻略(1)——Android基礎(chǔ)Android面試攻略(2)——異步消息處理機制Andr...
    黎清海閱讀 1,411評論 0 10
  • 一看到年齡和媽媽差不多大,生活狀態(tài)比媽媽好的阿姨,我就想到讓自己變的優(yōu)秀點,也讓媽媽以我為豪, 一是學習,一是改變...
    張嚴閱讀 369評論 0 0

友情鏈接更多精彩內(nèi)容