Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android性能優化典范之多線程篇

Android性能優化典范之多線程篇

編輯:關於Android編程

本文涉及的內容有:多線程並發的性能問題,介紹了 AsyncTask,HandlerThread,IntentService 與 ThreadPool 分別適合的使用場景以及各自的使用注意事項,這是一篇了解 Android 多線程編程不可多得的基礎文章,清楚的了解這些 Android 系統提供的多線程基礎組件之間的差異以及優缺點,才能夠在項目實戰中做出最恰當的選擇。

1. Threading Performance

在程序開發的實踐當中,為了讓程序表現得更加流暢,我們肯定會需要使用到多線程來提升程序的並發執行性能。但是編寫多線程並發的代碼一直以來都是一個相對棘手的問題,所以想要獲得更佳的程序性能,我們非常有必要掌握多線程並發編程的基礎技能。

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

 

騰訊bugly

 

一旦我們在主線程裡面添加了操作復雜的代碼,這些代碼就很可能阻礙主線程去響應點擊/滑動事件,阻礙主線程的 UI 繪制等等。我們知道,為了讓屏幕的刷新幀率達到 60fps,我們需要確保 16ms 內完成單次刷新的操作。一旦我們在主線程裡面執行的任務過於繁重就可能導致接收到刷新信號的時候因為資源被占用而無法完成這次刷新操作,這樣就會產生掉幀的現象,刷新幀率自然也就跟著下降了(一旦刷新幀率降到 20fps 左右,用戶就可以明顯感知到卡頓不流暢了)。

 

騰訊bugly

 

為了避免上面提到的掉幀問題,我們需要使用多線程的技術方案,把那些操作復雜的任務移動到其他線程當中執行,這樣就不容易阻塞主線程的操作,也就減小了出現掉幀的可能性。

 

騰訊bugly

 

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

AsyncTask: 為 UI 線程與工作線程之間進行快速的切換提供一種簡單便捷的機制。適用於當下立即需要啟動,但是異步執行的生命周期短暫的使用場景。 HandlerThread: 為某些回調方法或者等待某些任務的執行設置一個專屬的線程,並提供線程任務的調度機制。 ThreadPool: 把任務分解成不同的單元,分發到各個不同的線程上,進行同時並發處理。 IntentService: 適合於執行由 UI 觸發的後台 Service 任務,並可以把後台任務執行的情況通過一定的機制反饋給 UI。

了解這些系統提供的多線程工具類分別適合在什麼場景下,可以幫助我們選擇合適的解決方案,避免出現不可預期的麻煩。雖然使用多線程可以提高程序的並發量,但是我們需要特別注意因為引入多線程而可能伴隨而來的內存問題。舉個例子,在 Activity 內部定義的一個 AsyncTask,它屬於一個內部類,該類本身和外面的 Activity 是有引用關系的,如果 Activity 要銷毀的時候,AsyncTask 還仍然在運行,這會導致 Activity 沒有辦法完全釋放,從而引發內存洩漏。所以說,多線程是提升程序性能的有效手段之一,但是使用多線程卻需要十分謹慎小心,如果不了解背後的執行機制以及使用的注意事項,很可能引起嚴重的問題。

2. Understanding Android Threading

通常來說,一個線程需要經歷三個生命階段:開始,執行,結束。線程會在任務執行完畢之後結束,那麼為了確保線程的存活,我們會在執行階段給線程賦予不同的任務,然後在裡面添加退出的條件從而確保任務能夠執行完畢後退出。

 

騰訊bugly

 

在很多時候,線程不僅僅是線性執行一系列的任務就結束那麼簡單的,我們會需要增加一個任務隊列,讓線程不斷的從任務隊列中獲取任務去進行執行,另外我們還可能在線程執行的任務過程中與其他的線程進行協作。如果這些細節都交給我們自己來處理,這將會是件極其繁瑣又容易出錯的事情。

 

騰訊bugly

 

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

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

 

騰訊bugly

 

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

 

騰訊bugly

 

MessageQueue: 使用 Intent,Message,Runnable 作為任務的載體在不同的線程之間進行傳遞。

 

騰訊bugly

 

把上面三個組件打包到一起進行協作,這就是 HandlerThread

我們知道,當程序被啟動,系統會幫忙創建進程以及相應的主線程,而這個主線程其實就是一個 HandlerThread。這個主線程會需要處理系統事件,輸入事件,系統回調的任務,UI繪制等等任務,為了避免主線程任務過重,我們就會需要不斷的開啟新的工作線程來處理那些子任務。

3. Memory & Threading

增加並發的線程數會導致內存消耗的增加,平衡好這兩者的關系是非常重要的。我們知道,多線程並發訪問同一塊內存區域有可能帶來很多問題,例如讀寫的權限爭奪問題,ABA 問題等等。為了解決這些問題,我們會需要引入鎖的概念。

在 Android 系統中也無法避免因為多線程的引入而導致出現諸如上文提到的種種問題。Android UI 對象的創建,更新,銷毀等等操作都默認是執行在主線程,但是如果我們在非主線程對UI對象進行操作,程序將可能出現異常甚至是崩潰。

 

騰訊bugly

 

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

 

騰訊bugly

 

不僅如此,View 對象本身對所屬的 Activity 是有引用關系的,如果工作線程持續保有 View 的引用,這就可能導致 Activity 無法完全釋放。除了直接顯式的引用關系可能導致內存洩露之外,我們還需要特別留意隱式的引用關系也可能導致洩露。例如通常我們會看到在 Activity 裡面定義的一個 AsyncTask,這種類型的 AsyncTask 與外部的 Activity 是存在隱式引用關系的,只要 Task 沒有結束,引用關系就會一直存在,這很容易導致 Activity 的洩漏。更糟糕的情況是,它不僅僅發生了內存洩漏,還可能導致程序異常或者崩潰。

 

騰訊bugly

 

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

4. Good AsyncTask Hunting

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

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

 

騰訊bugly

 

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

 

騰訊bugly

 

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

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

 

騰訊bugly

 

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

 

騰訊bugly

 

其次,如何才能夠真正的取消一個 AsyncTask 的執行呢?我們知道 AsyncTaks 有提供 cancel()的方法,但是這個方法實際上做了什麼事情呢?線程本身並不具備中止正在執行的代碼的能力,為了能夠讓一個線程更早的被銷毀,我們需要在 doInBackground()的代碼中不斷的添加程序是否被中止的判斷邏輯,如下圖所示:

 

騰訊bugly

 

一旦任務被成功中止,AsyncTask 就不會繼續調用 onPostExecute(),而是通過調用 onCancelled()的回調方法反饋任務執行取消的結果。我們可以根據任務回調到哪個方法(是 onPostExecute 還是 onCancelled)來決定是對 UI 進行正常的更新還是把對應的任務所占用的內存進行銷毀等。

最後,使用 AsyncTask 很容易導致內存洩漏,一旦把 AsyncTask 寫成 Activity 的內部類的形式就很容易因為 AsyncTask 生命周期的不確定而導致 Activity 發生洩漏。

 

騰訊bugly

 

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

5. Getting a HandlerThread

大多數情況下,AsyncTask 都能夠滿足多線程並發的場景需要(在工作線程執行任務並返回結果到主線程),但是它並不是萬能的。例如打開相機之後的預覽幀數據是通過 onPreviewFrame()的方法進行回調的,onPreviewFrame()和 open()相機的方法是執行在同一個線程的。

 

騰訊bugly

 

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

 

騰訊bugly

 

我們需要確保 onPreviewFrame()執行在工作線程。如果使用 AsyncTask,會因為 AsyncTask 默認的線性執行的特性(即使換成並發執行)會導致因為無法把任務及時傳遞給工作線程而導致任務在主線程中被延遲,直到工作線程空閒,才可以把任務切換到工作線程中進行執行。

 

騰訊bugly

 

所以我們需要的是一個執行在工作線程,同時又能夠處理隊列中的復雜任務的功能,而 HandlerThread 的出現就是為了實現這個功能的,它組合了 Handler,MessageQueue,Looper 實現了一個長時間運行的線程,不斷的從隊列中獲取任務進行執行的功能。

 

騰訊bugly

 

回到剛才的處理相機回調數據的例子,使用 HandlerThread 我們可以把 open()操作與 onPreviewFrame()的操作執行在同一個線程,同時還避免了 AsyncTask 的弊端。如果需要在 onPreviewFrame()裡面更新 UI,只需要調用 runOnUiThread()方法把任務回調給主線程就夠了。

 

騰訊bugly

 

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

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

 

騰訊bugly

 

掌握 HandlerThread 與 AsyncTask 之間的優缺點,可以幫助我們選擇合適的方案。

6. Swimming in Threadpools

線程池適合用在把任務進行分解,並發進行執行的場景。通常來說,系統裡面會針對不同的任務設置一個單獨的守護線程用來專門處理這項任務。例如使用 Networking Thread 用來專門處理網絡請求的操作,使用 IO Thread 用來專門處理系統的 I\O 操作。針對那些場景,這樣設計是沒有問題的,因為對應的任務單次執行的時間並不長而且可以是順序執行的。但是這種專屬的單線程並不能滿足所有的情況,例如我們需要一次性 decode 40張圖片,每個線程需要執行 4ms 的時間,如果我們使用專屬單線程的方案,所有圖片執行完畢會需要花費 160ms(40*4),但是如果我們創建10個線程,每個線程執行4個任務,那麼我們就只需要16ms就能夠把所有的圖片處理完畢。

 

騰訊bugly

 

為了能夠實現上面的線程池模型,系統為我們提供了 ThreadPoolExecutor 幫助類來簡化實現,剩下需要做的就只是對任務進行分解就好了。

 

騰訊bugly

 

使用線程池需要特別注意同時並發線程數量的控制,理論上來說,我們可以設置任意你想要的並發數量,但是這樣做非常的不好。因為 CPU 只能同時執行固定數量的線程數,一旦同時並發的線程數量超過 CPU 能夠同時執行的阈值,CPU 就需要花費精力來判斷到底哪些線程的優先級比較高,需要在不同的線程之間進行調度切換。

 

騰訊bugly

 

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

 

騰訊bugly

 

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

7. The Zen of IntentService

默認的 Service 是執行在主線程的,可是通常情況下,這很容易影響到程序的繪制性能(搶占了主線程的資源)。除了前面介紹過的 AsyncTask 與 HandlerThread,我們還可以選擇使用 IntentService 來實現異步操作。IntentService 繼承自普通 Service 同時又在內部創建了一個 HandlerThread,在 onHandlerIntent()的回調裡面處理扔到 IntentService 的任務。所以 IntentService 就不僅僅具備了異步線程的特性,還同時保留了 Service 不受主頁面生命周期影響的特點。

 

騰訊bugly

 

如此一來,我們可以在 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 被銷毀之後,工作線程還繼續更新視圖是沒有意義的,因為此時視圖已經不在界面上顯示了。

 

騰訊bugly

 

Loader 的出現就是為了確保工作線程能夠和 Activity 的生命周期保持一致,同時避免出現前面提到的問題。

 

騰訊bugly

 

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

在 Activity 或者 Fragment 中使用 Loader 可以方便的實現異步加載的框架,Loader 有諸多優點。但是實現 Loader 的這套代碼還是稍微有點點復雜,Android 官方為我們提供了使用 Loader 的示例代碼進行參考學習。

9. The Importance of Thread Priority

理論上來說,我們的程序可以創建出非常多的子線程一起並發執行的,可是基於 CPU 時間片輪轉調度的機制,不可能所有的線程都可以同時被調度執行,CPU 需要根據線程的優先級賦予不同的時間片。

 

騰訊bugly

 

Android 系統會根據當前運行的可見的程序和不可見的後台程序對線程進行歸類,劃分為 forground 的那部分線程會大致占用掉 CPU 的90%左右的時間片,background 的那部分線程就總共只能分享到5%-10%左右的時間片。之所以設計成這樣是因為 forground 的程序本身的優先級就更高,理應得到更多的執行時間。

 

騰訊bugly

 

默認情況下,新創建的線程的優先級默認和創建它的母線程保持一致。如果主 UI 線程創建出了幾十個工作線程,這些工作線程的優先級就默認和主線程保持一致了,為了不讓新創建的工作線程和主線程搶占 CPU 資源,需要把這些線程的優先級進行降低處理,這樣才能給幫組 CPU 識別主次,提高主線程所能得到的系統資源。

 

騰訊bugly

 

在 Android 系統裡面,我們可以通過 android.os.Process.setThreadPriority(int) 設置線程的優先級,參數范圍從-20到19,數值越小優先級越高。Android 系統還為我們提供了以下的一些預設值,我們可以通過給不同的工作線程設置不同數值的優先級來達到更細粒度的控制。

 

騰訊bugly

 

大多數情況下,新創建的線程優先級會被設置為默認的0,主線程設置為0的時候,新創建的線程還可以利用 THREAD_PRIORITY_LESS_FAVORABLE 或者 THREAD_PRIORITY_MORE_FAVORABLE 來控制線程的優先級。

 

騰訊bugly

 

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

 

騰訊bugly

 

 

騰訊bugly

 

10. Profile GPU Rendering : M Update

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

 

騰訊bugly

 

但是僅僅顯示三大步驟的時間耗費情況,還是不太能夠清晰幫助我們定位具體的程序代碼問題,所以在 Android M 版本開始,GPU Profiling 工具把渲染操作拆解成如下8個詳細的步驟進行顯示。

 

騰訊bugly

 

舊版本中提到的 Proces,Execute,Update 還是繼續得到了保留,他們的對應關系如下:

 

騰訊bugly

 

接下去我們看下其他五個步驟分別代表了什麼含義:

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 的阈值線,這樣就很容易找出那些不合理的性能問題,再仔細看對應具體哪個步驟相對來說耗費時間比例更大,結合上面介紹的細化步驟,從而快速定位問題,修復問題。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved