Android性能優化典範 - 第5季

這是Android性能優化典範第5季的課程學習筆記,拖拖拉拉好久,記錄分享給你們,請多多包涵擔待指正!文章共10個段落,涉及的內容有:多線程併發的性能問題,介紹了AsyncTask,HandlerThread,IntentService與ThreadPool分別適合的使用場景以及各自的使用注意事項,這是一篇瞭解Android多線程編程不可多得的基礎文章,清楚的瞭解這些Android系統提供的多線程基礎組件之間的差別以及優缺點,纔可以在項目實戰中作出最恰當的選擇。html

1)Threading Performance

在程序開發的實踐當中,爲了讓程序表現得更加流暢,咱們確定會須要使用到多線程來提高程序的併發執行性能。可是編寫多線程併發的代碼一直以來都是一個相對棘手的問題,因此想要得到更佳的程序性能,咱們很是有必要掌握多線程併發編程的基礎技能。android

衆所周知,Android程序的大多數代碼操做都必須執行在主線程,例如系統事件(例如設備屏幕發生旋轉),輸入事件(例如用戶點擊滑動等),程序回調服務,UI繪製以及鬧鐘事件等等。那麼咱們在上述事件或者方法中插入的代碼也將執行在主線程。編程

android_perf_5_threading_main_thread

一旦咱們在主線程裏面添加了操做複雜的代碼,這些代碼就極可能阻礙主線程去響應點擊/滑動事件,阻礙主線程的UI繪製等等。咱們知道,爲了讓屏幕的刷新幀率達到60fps,咱們須要確保16ms內完成單次刷新的操做。一旦咱們在主線程裏面執行的任務過於繁重就可能致使接收到刷新信號的時候由於資源被佔用而沒法完成此次刷新操做,這樣就會產生掉幀的現象,刷新幀率天然也就跟着降低了(一旦刷新幀率降到20fps左右,用戶就能夠明顯感知到卡頓不流暢了)。緩存

android_perf_5_threading_dropframe

爲了不上面提到的掉幀問題,咱們須要使用多線程的技術方案,把那些操做複雜的任務移動到其餘線程當中執行,這樣就不容易阻塞主線程的操做,也就減少了出現掉幀的可能性。性能優化

android_perf_5_threading_workthread

那麼問題來了,爲主線程減輕負的多線程方案有哪些呢?這些方案分別適合在什麼場景下使用?Android系統爲咱們提供了若干組工具類來幫助解決這個問題。網絡

  • AsyncTask: 爲UI線程與工做線程之間進行快速的切換提供一種簡單便捷的機制。適用於當下當即須要啓動,可是異步執行的生命週期短暫的使用場景。
  • HandlerThread: 爲某些回調方法或者等待某些任務的執行設置一個專屬的線程,並提供線程任務的調度機制。
  • ThreadPool: 把任務分解成不一樣的單元,分發到各個不一樣的線程上,進行同時併發處理。
  • IntentService: 適合於執行由UI觸發的後臺Service任務,並能夠把後臺任務執行的狀況經過必定的機制反饋給UI。

瞭解這些系統提供的多線程工具類分別適合在什麼場景下,能夠幫助咱們選擇合適的解決方案,避免出現不可預期的麻煩。雖然使用多線程能夠提升程序的併發量,可是咱們須要特別注意由於引入多線程而可能伴隨而來的內存問題。舉個例子,在Activity內部定義的一個AsyncTask,它屬於一個內部類,該類自己和外面的Activity是有引用關係的,若是Activity要銷燬的時候,AsyncTask還仍然在運行,這會致使Activity沒有辦法徹底釋放,從而引起內存泄漏。因此說,多線程是提高程序性能的有效手段之一,可是使用多線程卻須要十分謹慎當心,若是不瞭解背後的執行機制以及使用的注意事項,極可能引發嚴重的問題。多線程

2)Understanding Android Threading

一般來講,一個線程須要經歷三個生命階段:開始,執行,結束。線程會在任務執行完畢以後結束,那麼爲了確保線程的存活,咱們會在執行階段給線程賦予不一樣的任務,而後在裏面添加退出的條件從而確保任務可以執行完畢後退出。併發

android_perf_5_thread_lifecycle

在不少時候,線程不只僅是線性執行一系列的任務就結束那麼簡單的,咱們會須要增長一個任務隊列,讓線程不斷的從任務隊列中獲取任務去進行執行,另外咱們還可能在線程執行的任務過程當中與其餘的線程進行協做。若是這些細節都交給咱們本身來處理,這將會是件極其繁瑣又容易出錯的事情。app

android_perf_5_thread_thread

所幸的是,Android系統爲咱們提供了Looper,Handler,MessageQueue來幫助實現上面的線程任務模型:框架

Looper: 可以確保線程持續存活而且能夠不斷的從任務隊列中獲取任務並進行執行。

android_perf_5_thread_looper

Handler: 可以幫助實現隊列任務的管理,不只僅可以把任務插入到隊列的頭部,尾部,還能夠按照必定的時間延遲來確保任務從隊列中可以來得及被取消掉。

android_perf_5_thread_handler

MessageQueue: 使用Intent,Message,Runnable做爲任務的載體在不一樣的線程之間進行傳遞。

android_perf_5_thread_messagequeue

把上面三個組件打包到一塊兒進行協做,這就是HandlerThread

android_perf_5_thread_handlerthread

咱們知道,當程序被啓動,系統會幫忙建立進程以及相應的主線程,而這個主線程其實就是一個HandlerThread。這個主線程會須要處理系統事件,輸入事件,系統回調的任務,UI繪製等等任務,爲了不主線程任務太重,咱們就會須要不斷的開啓新的工做線程來處理那些子任務。

3)Memory & Threading

增長併發的線程數會致使內存消耗的增長,平衡好這二者的關係是很是重要的。咱們知道,多線程併發訪問同一塊內存區域有可能帶來不少問題,例如讀寫的權限爭奪問題,ABA問題等等。爲了解決這些問題,咱們會須要引入的概念。

在Android系統中也沒法避免由於多線程的引入而致使出現諸如上文提到的種種問題。Android UI對象的建立,更新,銷燬等等操做都默認是執行在主線程,可是若是咱們在非主線程對UI對象進行操做,程序將可能出現異常甚至是崩潰。

android_perf_5_memory_thread_update

另外,在非UI線程中直接持有UI對象的引用也極可能出現問題。例如Work線程中持有某個UI對象的引用,在Work線程執行完畢以前,UI對象在主線程中被從ViewHierarchy中移除了,這個時候UI對象的任何屬性都已經再也不可用了,另外對這個UI對象的更新操做也都沒有任何意義了,由於它已經從ViewHierarchy中被移除,再也不繪製到畫面上了。

android_perf_5_memory_view_remove

不只如此,View對象自己對所屬的Activity是有引用關係的,若是工做線程持續保有View的引用,這就可能致使Activity沒法徹底釋放。除了直接顯式的引用關係可能致使內存泄露以外,咱們還須要特別留意隱式的引用關係也可能致使泄露。例如一般咱們會看到在Activity裏面定義的一個AsyncTask,這種類型的AsyncTask與外部的Activity是存在隱式引用關係的,只要Task沒有結束,引用關係就會一直存在,這很容易致使Activity的泄漏。更糟糕的狀況是,它不只僅發生了內存泄漏,還可能致使程序異常或者崩潰。

android_perf_5_memory_asynctask

爲了解決上面的問題,咱們須要謹記的原則就是:不要在任何非UI線程裏面去持有UI對象的引用。系統爲了確保全部的UI對象都只會被UI線程所進行建立,更新,銷燬的操做,特意設計了對應的工做機制(當Activity被銷燬的時候,由該Activity所觸發的非UI線程都將沒法對UI對象進行操做,否者就會拋出程序執行異常的錯誤)來防止UI對象被錯誤的使用。

4)Good AsyncTask Hunting

AsyncTask是一個讓人既愛又恨的組件,它提供了一種簡便的異步處理機制,可是它又同時引入了一些使人厭惡的麻煩。一旦對AsyncTask使用不當,極可能對程序的性能帶來負面影響,同時還可能致使內存泄露。

舉個例子,常遇到的一個典型的使用場景:用戶切換到某個界面,觸發了界面上的圖片的加載操做,由於圖片的加載相對來講耗時比較長,咱們須要在子線程中處理圖片的加載,當圖片在子線程中處理完成以後,再把處理好的圖片返回給主線程,交給UI更新到畫面上。

android_perf_5_asynctask_main

AsyncTask的出現就是爲了快速的實現上面的使用場景,AsyncTask把在主線程裏面的準備工做放到onPreExecute()方法裏面進行執行,doInBackground()方法執行在工做線程中,用來處理那些繁重的任務,一旦任務執行完畢,就會調用onPostExecute()方法返回到主線程。

android_perf_5_asynctask_mode

使用AsyncTask須要注意的問題有哪些呢?請關注如下幾點:

  • 首先,默認狀況下,全部的AsyncTask任務都是被線性調度執行的,他們處在同一個任務隊列當中,按順序逐個執行。假設你按照順序啓動20個AsyncTask,一旦其中的某個AsyncTask執行時間過長,隊列中的其餘剩餘AsyncTask都處於阻塞狀態,必須等到該任務執行完畢以後纔可以有機會執行下一個任務。狀況以下圖所示:

android_perf_5_asynctask_single_queue

爲了解決上面提到的線性隊列等待的問題,咱們可使用AsyncTask.executeOnExecutor()強制指定AsyncTask使用線程池併發調度任務。

android_perf_5_asynctask_thread_pool

  • 其次,如何纔可以真正的取消一個AsyncTask的執行呢?咱們知道AsyncTaks有提供cancel()的方法,可是這個方法實際上作了什麼事情呢?線程自己並不具有停止正在執行的代碼的能力,爲了可以讓一個線程更早的被銷燬,咱們須要在doInBackground()的代碼中不斷的添加程序是否被停止的判斷邏輯,以下圖所示:

android_perf_5_asynctask_cancel

一旦任務被成功停止,AsyncTask就不會繼續調用onPostExecute(),而是經過調用onCancelled()的回調方法反饋任務執行取消的結果。咱們能夠根據任務回調到哪一個方法(是onPostExecute仍是onCancelled)來決定是對UI進行正常的更新仍是把對應的任務所佔用的內存進行銷燬等。

  • 最後,使用AsyncTask很容易致使內存泄漏,一旦把AsyncTask寫成Activity的內部類的形式就很容易由於AsyncTask生命週期的不肯定而致使Activity發生泄漏。

android_perf_5_memory_asynctask

綜上所述,AsyncTask雖然提供了一種簡單便捷的異步機制,可是咱們仍是頗有必要特別關注到他的缺點,避免出現由於使用錯誤而致使的嚴重系統性能問題。

5)Getting a HandlerThread

大多數狀況下,AsyncTask都可以知足多線程併發的場景須要(在工做線程執行任務並返回結果到主線程),可是它並非萬能的。例如打開相機以後的預覽幀數據是經過onPreviewFrame()的方法進行回調的,onPreviewFrame()open()相機的方法是執行在同一個線程的。

android_perf_5_handlerthread_camera_open

若是這個回調方法執行在UI線程,那麼在onPreviewFrame()裏面將要執行的數據轉換操做將和主線程的界面繪製,事件傳遞等操做爭搶系統資源,這就有可能影響到主界面的表現性能。

android_perf_5_handlerthread_main_thread2

咱們須要確保onPreviewFrame()執行在工做線程。若是使用AsyncTask,會由於AsyncTask默認的線性執行的特性(即便換成併發執行)會致使由於沒法把任務及時傳遞給工做線程而致使任務在主線程中被延遲,直到工做線程空閒,才能夠把任務切換到工做線程中進行執行。

android_perf_5_handlerthread_asynctask

因此咱們須要的是一個執行在工做線程,同時又可以處理隊列中的複雜任務的功能,而HandlerThread的出現就是爲了實現這個功能的,它組合了Handler,MessageQueue,Looper實現了一個長時間運行的線程,不斷的從隊列中獲取任務進行執行的功能。

android_perf_5_handlerthread_outline

回到剛纔的處理相機回調數據的例子,使用HandlerThread咱們能夠把open()操做與onPreviewFrame()的操做執行在同一個線程,同時還避免了AsyncTask的弊端。若是須要在onPreviewFrame()裏面更新UI,只須要調用runOnUiThread()方法把任務回調給主線程就夠了。

android_perf_5_handlerthread_camera

HandlerThread比較合適處理那些在工做線程執行,須要花費時間偏長的任務。咱們只須要把任務發送給HandlerThread,而後就只須要等待任務執行結束的時候通知返回到主線程就行了。

另外很重要的一點是,一旦咱們使用了HandlerThread,須要特別注意給HandlerThread設置不一樣的線程優先級,CPU會根據設置的不一樣線程優先級對全部的線程進行調度優化。

android_perf_5_handlerthread_priority

掌握HandlerThread與AsyncTask之間的優缺點,能夠幫助咱們選擇合適的方案。

6)Swimming in Threadpools

線程池適合用在把任務進行分解,併發進行執行的場景。一般來講,系統裏面會針對不一樣的任務設置一個單獨的守護線程用來專門處理這項任務。例如使用Networking Thread用來專門處理網絡請求的操做,使用IO Thread用來專門處理系統的I\O操做。針對那些場景,這樣設計是沒有問題的,由於對應的任務單次執行的時間並不長並且能夠是順序執行的。可是這種專屬的單線程並不能知足全部的狀況,例如咱們須要一次性decode 40張圖片,每一個線程須要執行4ms的時間,若是咱們使用專屬單線程的方案,全部圖片執行完畢會須要花費160ms(40*4),可是若是咱們建立10個線程,每一個線程執行4個任務,那麼咱們就只須要16ms就可以把全部的圖片處理完畢。

android_perf_5_threadpool_1

爲了可以實現上面的線程池模型,系統爲咱們提供了ThreadPoolExecutor幫助類來簡化實現,剩下須要作的就只是對任務進行分解就行了。

android_perf_5_threadpool_2

使用線程池須要特別注意同時併發線程數量的控制,理論上來講,咱們能夠設置任意你想要的併發數量,可是這樣作很是的很差。由於CPU只能同時執行固定數量的線程數,一旦同時併發的線程數量超過CPU可以同時執行的閾值,CPU就須要花費精力來判斷到底哪些線程的優先級比較高,須要在不一樣的線程之間進行調度切換。

android_perf_5_threadpool_3

一旦同時併發的線程數量達到必定的量級,這個時候CPU在不一樣線程之間進行調度的時間就可能過長,反而致使性能嚴重降低。另外須要關注的一點是,每開一個新的線程,都會耗費至少64K+的內存。爲了可以方便的對線程數量進行控制,ThreadPoolExecutor爲咱們提供了初始化的併發線程數量,以及最大的併發數量進行設置。

android_perf_5_threadpool_4

另外須要關注的一個問題是:Runtime.getRuntime().availableProcesser()方法並不可靠,他返回的值並非真實的CPU核心數,由於CPU會在某些狀況下選擇對部分核心進行睡眠處理,在這種狀況下,返回的數量就只能是激活的CPU核心數。

7)The Zen of IntentService

默認的Service是執行在主線程的,但是一般狀況下,這很容易影響到程序的繪製性能(搶佔了主線程的資源)。除了前面介紹過的AsyncTask與HandlerThread,咱們還能夠選擇使用IntentService來實現異步操做。IntentService繼承自普通Service同時又在內部建立了一個HandlerThread,在onHandlerIntent()的回調裏面處理扔到IntentService的任務。因此IntentService就不只僅具有了異步線程的特性,還同時保留了Service不受主頁面生命週期影響的特色。

android_perf_5_intentservice_outline

如此一來,咱們能夠在IntentService裏面經過設置鬧鐘間隔性的觸發異步任務,例如刷新數據,更新緩存的圖片或者是分析用戶操做行爲等等,固然處理這些任務須要當心謹慎。

使用IntentService須要特別留意如下幾點:

  • 首先,由於IntentService內置的是HandlerThread做爲異步線程,因此每個交給IntentService的任務都將以隊列的方式逐個被執行到,一旦隊列中有某個任務執行時間過長,那麼就會致使後續的任務都會被延遲處理。
  • 其次,一般使用到IntentService的時候,咱們會結合使用BroadcastReceiver把工做線程的任務執行結果返回給主UI線程。使用廣播容易引發性能問題,咱們可使用LocalBroadcastManager來發送只在程序內部傳遞的廣播,從而提高廣播的性能。咱們也可使用runOnUiThread()快速回調到主UI線程。
  • 最後,包含正在運行的IntentService的程序相比起純粹的後臺程序更不容易被系統殺死,該程序的優先級是介於前臺程序與純後臺程序之間的。

8)Threading and Loaders

當啓動工做線程的Activity被銷燬的時候,咱們應該作點什麼呢?爲了方便的控制工做線程的啓動與結束,Android爲咱們引入了Loader來解決這個問題。咱們知道Activity有可能由於用戶的主動切換而頻繁的被建立與銷燬,也有多是由於相似屏幕發生旋轉等被動緣由而銷燬再重建。在Activity不停的建立與銷燬的過程中,頗有可能由於工做線程持有Activity的View而致使內存泄漏(由於工做線程極可能持有View的強引用,另外工做線程的生命週期還沒法保證和Activity的生命週期一致,這樣就容易發生內存泄漏了)。除了可能引發內存泄漏以外,在Activity被銷燬以後,工做線程還繼續更新視圖是沒有意義的,由於此時視圖已經不在界面上顯示了。

android_perf_5_loader_bad

Loader的出現就是爲了確保工做線程可以和Activity的生命週期保持一致,同時避免出現前面提到的問題。

android_perf_5_loader_good

LoaderManager會對查詢的操做進行緩存,只要對應Cursor上的數據源沒有發生變化,在配置信息發生改變的時候(例如屏幕的旋轉),Loader能夠直接把緩存的數據回調到onLoadFinished(),從而避免從新查詢數據。另外系統會在Loader再也不須要使用到的時候(例如使用Back按鈕退出當前頁面)回調onLoaderReset()方法,咱們能夠在這裏作數據的清除等等操做。

在Activity或者Fragment中使用Loader能夠方便的實現異步加載的框架,Loader有諸多優勢。可是實現Loader的這套代碼仍是稍微有點點複雜,Android官方爲咱們提供了使用Loader的示例代碼進行參考學習。

9)The Importance of Thread Priority

理論上來講,咱們的程序能夠建立出很是多的子線程一塊兒併發執行的,但是基於CPU時間片輪轉調度的機制,不可能全部的線程均可以同時被調度執行,CPU須要根據線程的優先級賦予不一樣的時間片。

android_perf_5_threadpriority_CPU

Android系統會根據當前運行的可見的程序和不可見的後臺程序對線程進行歸類,劃分爲forground的那部分線程會大體佔用掉CPU的90%左右的時間片,background的那部分線程就總共只能分享到5%-10%左右的時間片。之因此設計成這樣是由於forground的程序自己的優先級就更高,理應獲得更多的執行時間。

android_perf_5_threadpriority_90

默認狀況下,新建立的線程的優先級默認和建立它的母線程保持一致。若是主UI線程建立出了幾十個工做線程,這些工做線程的優先級就默認和主線程保持一致了,爲了避免讓新建立的工做線程和主線程搶佔CPU資源,須要把這些線程的優先級進行下降處理,這樣才能給幫組CPU識別主次,提升主線程所能獲得的系統資源。

android_perf_5_threadpriority_less

在Android系統裏面,咱們能夠經過android.os.Process.setThreadPriority(int)設置線程的優先級,參數範圍從-20到24,數值越小優先級越高。Android系統還爲咱們提供瞭如下的一些預設值,咱們能夠經過給不一樣的工做線程設置不一樣數值的優先級來達到更細粒度的控制。

android_perf_5_threadpriority_const

大多數狀況下,新建立的線程優先級會被設置爲默認的0,主線程設置爲0的時候,新建立的線程還能夠利用THREAD_PRIORITY_LESS_FAVORABLE或者THREAD_PRIORITY_MORE_FAVORABLE來控制線程的優先級。

android_perf_5_threadpriority_value

Android系統裏面的AsyncTask與IntentService已經默認幫助咱們設置線程的優先級,可是對於那些非官方提供的多線程工具類,咱們須要特別留意根據須要本身手動來設置線程的優先級。

android_perf_5_threadpriority_asynctask android_perf_5_threadpriority_intentservice

10)Profile GPU Rendering : M Update

從Android M系統開始,系統更新了GPU Profiling的工具來幫助咱們定位UI的渲染性能問題。早期的CPU Profiling工具只能粗略的顯示出Process,Execute,Update三大步驟的時間耗費狀況。

android_perf_5_gpu_profiling_old

可是僅僅顯示三大步驟的時間耗費狀況,仍是不太可以清晰幫助咱們定位具體的程序代碼問題,因此在Android M版本開始,GPU Profiling工具把渲染操做拆解成以下8個詳細的步驟進行顯示。

android_perf_5_gpu_profiling_8steps

舊版本中提到的Proces,Execute,Update仍是繼續獲得了保留,他們的對應關係以下:

android_perf_5_gpu_profiling_3steps

接下去咱們看下其餘五個步驟分別表明了什麼含義:

  • Sync & Upload:一般表示的是準備當前界面上有待繪製的圖片所耗費的時間,爲了減小該段區域的執行時間,咱們能夠減小屏幕上的圖片數量或者是縮小圖片自己的大小。
  • Measure & Layout:這裏表示的是佈局的onMeasure與onLayout所花費的時間,一旦時間過長,就須要仔細檢查本身的佈局是否是存在嚴重的性能問題。
  • Animation:表示的是計算執行動畫所須要花費的時間,包含的動畫有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦這裏的執行時間過長,就須要檢查是否是使用了非官方的動畫工具或者是檢查動畫執行的過程當中是否是觸發了讀寫操做等等。
  • Input Handling:表示的是系統處理輸入事件所耗費的時間,粗略等於對於的事件處理方法所執行的時間。一旦執行時間過長,意味着在處理用戶的輸入事件的地方執行了複雜的操做。
  • Misc/Vsync Delay:若是稍加註意,咱們能夠在開發應用的Log日誌裏面看到這樣一行提示:I/Choreographer(691): Skipped XXX frames! The application may be doing too much work on its main thread。這意味着咱們在主線程執行了太多的任務,致使UI渲染跟不上vSync的信號而出現掉幀的狀況。

上面八種不一樣的顏色區分了不一樣的操做所耗費的時間,爲了便於咱們迅速找出那些有問題的步驟,GPU Profiling工具會顯示16ms的閾值線,這樣就很容易找出那些不合理的性能問題,再仔細看對應具體哪一個步驟相對來講耗費時間比例更大,結合上面介紹的細化步驟,從而快速定位問題,修復問題。

相關文章
相關標籤/搜索