本文涉及的內容有:多線程併發的性能問題,介紹了 AsyncTask,HandlerThread,IntentService 與 ThreadPool 分別適合的使用場景以及各自的使用注意事項,這是一篇瞭解 Android 多線程編程不可多得的基礎文章,清楚的瞭解這些 Android 系統提供的多線程基礎組件之間的差別以及優缺點,纔可以在項目實戰中作出最恰當的選擇。android
1. Threading Performance
在程序開發的實踐當中,爲了讓程序表現得更加流暢,咱們確定會須要使用到多線程來提高程序的併發執行性能。可是編寫多線程併發的代碼一直以來都是一個相對棘手的問題,因此想要得到更佳的程序性能,咱們很是有必要掌握多線程併發編程的基礎技能。
衆所周知,Android 程序的大多數代碼操做都必須執行在主線程,例如系統事件(例如設備屏幕發生旋轉),輸入事件(例如用戶點擊滑動等),程序回調服務,UI 繪製以及鬧鐘事件等等。那麼咱們在上述事件或者方法中插入的代碼也將執行在主線程。
一旦咱們在主線程裏面添加了操做複雜的代碼,這些代碼就極可能阻礙主線程去響應點擊/滑動事件,阻礙主線程的 UI 繪製等等。咱們知道,爲了讓屏幕的刷新幀率達到 60fps,咱們須要確保 16ms 內完成單次刷新的操做。一旦咱們在主線程裏面執行的任務過於繁重就可能致使接收到刷新信號的時候由於資源被佔用而沒法完成此次刷新操做,這樣就會產生掉幀的現象,刷新幀率天然也就跟着降低了(一旦刷新幀率降到 20fps 左右,用戶就能夠明顯感知到卡頓不流暢了)。
爲了不上面提到的掉幀問題,咱們須要使用多線程的技術方案,把那些操做複雜的任務移動到其餘線程當中執行,這樣就不容易阻塞主線程的操做,也就減少了出現掉幀的可能性。
那麼問題來了,爲主線程減輕負的多線程方案有哪些呢?這些方案分別適合在什麼場景下使用?Android 系統爲咱們提供了若干組工具類來幫助解決這個問題。編程
- AsyncTask: 爲 UI 線程與工做線程之間進行快速的切換提供一種簡單便捷的機制。適用於當下當即須要啓動,可是異步執行的生命週期短暫的使用場景。
- HandlerThread: 爲某些回調方法或者等待某些任務的執行設置一個專屬的線程,並提供線程任務的調度機制。
- ThreadPool: 把任務分解成不一樣的單元,分發到各個不一樣的線程上,進行同時併發處理。
- IntentService: 適合於執行由 UI 觸發的後臺 Service 任務,並能夠把後臺任務執行的狀況經過必定的機制反饋給 UI。
瞭解這些系統提供的多線程工具類分別適合在什麼場景下,能夠幫助咱們選擇合適的解決方案,避免出現不可預期的麻煩。雖然使用多線程能夠提升程序的併發量,可是咱們須要特別注意由於引入多線程而可能伴隨而來的內存問題。舉個例子,在 Activity 內部定義的一個 AsyncTask,它屬於一個內部類,該類自己和外面的 Activity 是有引用關係的,若是 Activity 要銷燬的時候,AsyncTask 還仍然在運行,這會致使 Activity 沒有辦法徹底釋放,從而引起內存泄漏。因此說,多線程是提高程序性能的有效手段之一,可是使用多線程卻須要十分謹慎當心,若是不瞭解背後的執行機制以及使用的注意事項,極可能引發嚴重的問題。2. Understanding Android Threading
一般來講,一個線程須要經歷三個生命階段:開始,執行,結束。線程會在任務執行完畢以後結束,那麼爲了確保線程的存活,咱們會在執行階段給線程賦予不一樣的任務,而後在裏面添加退出的條件從而確保任務可以執行完畢後退出。
在不少時候,線程不只僅是線性執行一系列的任務就結束那麼簡單的,咱們會須要增長一個任務隊列,讓線程不斷的從任務隊列中獲取任務去進行執行,另外咱們還可能在線程執行的任務過程當中與其餘的線程進行協做。若是這些細節都交給咱們本身來處理,這將會是件極其繁瑣又容易出錯的事情。
所幸的是,Android 系統爲咱們提供了 Looper,Handler,MessageQueue 來幫助實現上面的線程任務模型:
Looper: 可以確保線程持續存活而且能夠不斷的從任務隊列中獲取任務並進行執行。
Handler: 可以幫助實現隊列任務的管理,不只僅可以把任務插入到隊列的頭部,尾部,還能夠按照必定的時間延遲來確保任務從隊列中可以來得及被取消掉。
MessageQueue: 使用 Intent,Message,Runnable 做爲任務的載體在不一樣的線程之間進行傳遞。
把上面三個組件打包到一塊兒進行協做,這就是 HandlerThread
咱們知道,當程序被啓動,系統會幫忙建立進程以及相應的主線程,而這個主線程其實就是一個 HandlerThread。這個主線程會須要處理系統事件,輸入事件,系統回調的任務,UI繪製等等任務,爲了不主線程任務太重,咱們就會須要不斷的開啓新的工做線程來處理那些子任務。3. Memory & Threading
增長併發的線程數會致使內存消耗的增長,平衡好這二者的關係是很是重要的。咱們知道,多線程併發訪問同一塊內存區域有可能帶來不少問題,例如讀寫的權限爭奪問題,ABA 問題等等。爲了解決這些問題,咱們會須要引入鎖的概念。
在 Android 系統中也沒法避免由於多線程的引入而致使出現諸如上文提到的種種問題。Android UI 對象的建立,更新,銷燬等等操做都默認是執行在主線程,可是若是咱們在非主線程對UI對象進行操做,程序將可能出現異常甚至是崩潰。
另外,在非 UI 線程中直接持有 UI 對象的引用也極可能出現問題。例如Work線程中持有某個 UI 對象的引用,在 Work 線程執行完畢以前,UI 對象在主線程中被從 ViewHierarchy 中移除了,這個時候 UI 對象的任何屬性都已經再也不可用了,另外對這個 UI 對象的更新操做也都沒有任何意義了,由於它已經從 ViewHierarchy 中被移除,再也不繪製到畫面上了。
不只如此,View 對象自己對所屬的 Activity 是有引用關係的,若是工做線程持續保有 View 的引用,這就可能致使 Activity 沒法徹底釋放。除了直接顯式的引用關係可能致使內存泄露以外,咱們還須要特別留意隱式的引用關係也可能致使泄露。例如一般咱們會看到在 Activity 裏面定義的一個 AsyncTask,這種類型的 AsyncTask 與外部的 Activity 是存在隱式引用關係的,只要 Task 沒有結束,引用關係就會一直存在,這很容易致使 Activity 的泄漏。更糟糕的狀況是,它不只僅發生了內存泄漏,還可能致使程序異常或者崩潰。
爲了解決上面的問題,咱們須要謹記的原則就是:不要在任何非 UI 線程裏面去持有 UI 對象的引用。系統爲了確保全部的 UI 對象都只會被 UI 線程所進行建立,更新,銷燬的操做,特意設計了對應的工做機制(當 Activity 被銷燬的時候,由該 Activity 所觸發的非 UI 線程都將沒法對UI對象進行操做,否者就會拋出程序執行異常的錯誤)來防止 UI 對象被錯誤的使用。4. Good AsyncTask Hunting
AsyncTask 是一個讓人既愛又恨的組件,它提供了一種簡便的異步處理機制,可是它又同時引入了一些使人厭惡的麻煩。一旦對 AsyncTask 使用不當,極可能對程序的性能帶來負面影響,同時還可能致使內存泄露。
舉個例子,常遇到的一個典型的使用場景:用戶切換到某個界面,觸發了界面上的圖片的加載操做,由於圖片的加載相對來講耗時比較長,咱們須要在子線程中處理圖片的加載,當圖片在子線程中處理完成以後,再把處理好的圖片返回給主線程,交給 UI 更新到畫面上。
AsyncTask 的出現就是爲了快速的實現上面的使用場景,AsyncTask 把在主線程裏面的準備工做放到onPreExecute()
方法裏面進行執行,doInBackground()
方法執行在工做線程中,用來處理那些繁重的任務,一旦任務執行完畢,就會調用onPostExecute()
方法返回到主線程。
使用 AsyncTask 須要注意的問題有哪些呢?請關注如下幾點:
首先,默認狀況下,全部的 AsyncTask 任務都是被線性調度執行的,他們處在同一個任務隊列當中,按順序逐個執行。假設你按照順序啓動20個 AsyncTask,一旦其中的某個 AsyncTask 執行時間過長,隊列中的其餘剩餘 AsyncTask 都處於阻塞狀態,必須等到該任務執行完畢以後纔可以有機會執行下一個任務。狀況以下圖所示:
爲了解決上面提到的線性隊列等待的問題,咱們可使用AsyncTask.executeOnExecutor()
強制指定 AsyncTask 使用線程池併發調度任務。
其次,如何纔可以真正的取消一個 AsyncTask 的執行呢?咱們知道 AsyncTaks 有提供cancel()
的方法,可是這個方法實際上作了什麼事情呢?線程自己並不具有停止正在執行的代碼的能力,爲了可以讓一個線程更早的被銷燬,咱們須要在doInBackground()
的代碼中不斷的添加程序是否被停止的判斷邏輯,以下圖所示:
一旦任務被成功停止,AsyncTask 就不會繼續調用onPostExecute()
,而是經過調用onCancelled()
的回調方法反饋任務執行取消的結果。咱們能夠根據任務回調到哪一個方法(是 onPostExecute 仍是 onCancelled)來決定是對 UI 進行正常的更新仍是把對應的任務所佔用的內存進行銷燬等。
最後,使用 AsyncTask 很容易致使內存泄漏,一旦把 AsyncTask 寫成 Activity 的內部類的形式就很容易由於 AsyncTask 生命週期的不肯定而致使 Activity 發生泄漏。
綜上所述,AsyncTask 雖然提供了一種簡單便捷的異步機制,可是咱們仍是頗有必要特別關注到他的缺點,避免出現由於使用錯誤而致使的嚴重系統性能問題。5. Getting a HandlerThread
大多數狀況下,AsyncTask 都可以知足多線程併發的場景須要(在工做線程執行任務並返回結果到主線程),可是它並非萬能的。例如打開相機以後的預覽幀數據是經過onPreviewFrame()
的方法進行回調的,onPreviewFrame()
和open()
相機的方法是執行在同一個線程的。
若是這個回調方法執行在 UI 線程,那麼在 onPreviewFrame()裏面將要執行的數據轉換操做將和主線程的界面繪製,事件傳遞等操做爭搶系統資源,這就有可能影響到主界面的表現性能。
咱們須要確保 onPreviewFrame()執行在工做線程。若是使用 AsyncTask,會由於 AsyncTask 默認的線性執行的特性(即便換成併發執行)會致使由於沒法把任務及時傳遞給工做線程而致使任務在主線程中被延遲,直到工做線程空閒,才能夠把任務切換到工做線程中進行執行。
因此咱們須要的是一個執行在工做線程,同時又可以處理隊列中的複雜任務的功能,而 HandlerThread 的出現就是爲了實現這個功能的,它組合了 Handler,MessageQueue,Looper 實現了一個長時間運行的線程,不斷的從隊列中獲取任務進行執行的功能。
回到剛纔的處理相機回調數據的例子,使用 HandlerThread 咱們能夠把 open()操做與 onPreviewFrame()的操做執行在同一個線程,同時還避免了 AsyncTask 的弊端。若是須要在 onPreviewFrame()裏面更新 UI,只須要調用 runOnUiThread()方法把任務回調給主線程就夠了。
HandlerThread 比較合適處理那些在工做線程執行,須要花費時間偏長的任務。咱們只須要把任務發送給 HandlerThread,而後就只須要等待任務執行結束的時候通知返回到主線程就行了。
另外很重要的一點是,一旦咱們使用了 HandlerThread,須要特別注意給 HandlerThread 設置不一樣的線程優先級,CPU 會根據設置的不一樣線程優先級對全部的線程進行調度優化。
掌握 HandlerThread 與 AsyncTask 之間的優缺點,能夠幫助咱們選擇合適的方案。6. Swimming in Threadpools
線程池適合用在把任務進行分解,併發進行執行的場景。一般來講,系統裏面會針對不一樣的任務設置一個單獨的守護線程用來專門處理這項任務。例如使用 Networking Thread 用來專門處理網絡請求的操做,使用 IO Thread 用來專門處理系統的 I\O 操做。針對那些場景,這樣設計是沒有問題的,由於對應的任務單次執行的時間並不長並且能夠是順序執行的。可是這種專屬的單線程並不能知足全部的狀況,例如咱們須要一次性 decode 40張圖片,每一個線程須要執行 4ms 的時間,若是咱們使用專屬單線程的方案,全部圖片執行完畢會須要花費 160ms(40*4),可是若是咱們建立10個線程,每一個線程執行4個任務,那麼咱們就只須要16ms就可以把全部的圖片處理完畢。
爲了可以實現上面的線程池模型,系統爲咱們提供了ThreadPoolExecutor
幫助類來簡化實現,剩下須要作的就只是對任務進行分解就行了。
使用線程池須要特別注意同時併發線程數量的控制,理論上來講,咱們能夠設置任意你想要的併發數量,可是這樣作很是的很差。由於 CPU 只能同時執行固定數量的線程數,一旦同時併發的線程數量超過 CPU 可以同時執行的閾值,CPU 就須要花費精力來判斷到底哪些線程的優先級比較高,須要在不一樣的線程之間進行調度切換。
一旦同時併發的線程數量達到必定的量級,這個時候 CPU 在不一樣線程之間進行調度的時間就可能過長,反而致使性能嚴重降低。另外須要關注的一點是,每開一個新的線程,都會耗費至少 64K+ 的內存。爲了可以方便的對線程數量進行控制,ThreadPoolExecutor 爲咱們提供了初始化的併發線程數量,以及最大的併發數量進行設置。
另外須要關注的一個問題是:Runtime.getRuntime().availableProcesser()
方法並不可靠,他返回的值並非真實的 CPU 核心數,由於 CPU 會在某些狀況下選擇對部分核心進行睡眠處理,在這種狀況下,返回的數量就只能是激活的 CPU 核心數。7. The Zen of IntentService
默認的 Service 是執行在主線程的,但是一般狀況下,這很容易影響到程序的繪製性能(搶佔了主線程的資源)。除了前面介紹過的 AsyncTask 與 HandlerThread,咱們還能夠選擇使用 IntentService 來實現異步操做。IntentService 繼承自普通 Service 同時又在內部建立了一個 HandlerThread,在onHandlerIntent()
的回調裏面處理扔到 IntentService 的任務。因此 IntentService 就不只僅具有了異步線程的特性,還同時保留了 Service 不受主頁面生命週期影響的特色。
如此一來,咱們能夠在 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 被銷燬以後,工做線程還繼續更新視圖是沒有意義的,由於此時視圖已經不在界面上顯示了。
Loader 的出現就是爲了確保工做線程可以和 Activity 的生命週期保持一致,同時避免出現前面提到的問題。
LoaderManager 會對查詢的操做進行緩存,只要對應 Cursor 上的數據源沒有發生變化,在配置信息發生改變的時候(例如屏幕的旋轉),Loader 能夠直接把緩存的數據回調到onLoadFinished()
,從而避免從新查詢數據。另外系統會在 Loader 再也不須要使用到的時候(例如使用 Back 按鈕退出當前頁面)回調onLoaderReset()
方法,咱們能夠在這裏作數據的清除等等操做。
在 Activity 或者 Fragment 中使用 Loader 能夠方便的實現異步加載的框架,Loader 有諸多優勢。可是實現 Loader 的這套代碼仍是稍微有點點複雜,Android 官方爲咱們提供了使用 Loader 的示例代碼進行參考學習。9. The Importance of Thread Priority
理論上來講,咱們的程序能夠建立出很是多的子線程一塊兒併發執行的,但是基於 CPU 時間片輪轉調度的機制,不可能全部的線程均可以同時被調度執行,CPU 須要根據線程的優先級賦予不一樣的時間片。
Android 系統會根據當前運行的可見的程序和不可見的後臺程序對線程進行歸類,劃分爲 forground 的那部分線程會大體佔用掉 CPU 的90%左右的時間片,background 的那部分線程就總共只能分享到5%-10%左右的時間片。之因此設計成這樣是由於 forground 的程序自己的優先級就更高,理應獲得更多的執行時間。
默認狀況下,新建立的線程的優先級默認和建立它的母線程保持一致。若是主 UI 線程建立出了幾十個工做線程,這些工做線程的優先級就默認和主線程保持一致了,爲了避免讓新建立的工做線程和主線程搶佔 CPU 資源,須要把這些線程的優先級進行下降處理,這樣才能給幫組 CPU 識別主次,提升主線程所能獲得的系統資源。
在 Android 系統裏面,咱們能夠經過android.os.Process.setThreadPriority(int)
設置線程的優先級,參數範圍從-20到19,數值越小優先級越高。Android 系統還爲咱們提供瞭如下的一些預設值,咱們能夠經過給不一樣的工做線程設置不一樣數值的優先級來達到更細粒度的控制。
大多數狀況下,新建立的線程優先級會被設置爲默認的0,主線程設置爲0的時候,新建立的線程還能夠利用THREAD_PRIORITY_LESS_FAVORABLE
或者THREAD_PRIORITY_MORE_FAVORABLE
來控制線程的優先級。
Android 系統裏面的 AsyncTask 與 IntentService已經默認幫助咱們設置線程的優先級,可是對於那些非官方提供的多線程工具類,咱們須要特別留意根據須要本身手動來設置線程的優先級。
![]()
10. Profile GPU Rendering : M Update
從 Android M 系統開始,系統更新了 GPU Profiling 的工具來幫助咱們定位 UI 的渲染性能問題。早期的 CPU Profiling 工具只能粗略的顯示出 Process,Execute,Update 三大步驟的時間耗費狀況。
可是僅僅顯示三大步驟的時間耗費狀況,仍是不太可以清晰幫助咱們定位具體的程序代碼問題,因此在 Android M 版本開始,GPU Profiling 工具把渲染操做拆解成以下8個詳細的步驟進行顯示。
舊版本中提到的 Proces,Execute,Update 仍是繼續獲得了保留,他們的對應關係以下:
接下去咱們看下其餘五個步驟分別表明了什麼含義:- 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 的閾值線,這樣就很容易找出那些不合理的性能問題,再仔細看對應具體哪一個步驟相對來講耗費時間比例更大,結合上面介紹的細化步驟,從而快速定位問題,修復問題。
若是你以爲內容意猶未盡,若是你想了解更多相關信息,請掃描如下二維碼,關注咱們的公衆帳號,能夠獲取更多技術類乾貨,還有精彩活動與你分享~緩存
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!markdown