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

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中線程優先級

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

歡迎關注
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容

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