Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android App卡頓慢優化之多線程優化

Android App卡頓慢優化之多線程優化

編輯:關於Android編程

本博客涉及的內容有:多線程並發的性能問題,介紹了AsyncTask,HandlerThread,IntentService與ThreadPool分別適合的使用場景以及各自的使用注意事項,這是一篇了解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線程)

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

\

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

 

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

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

\

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

\

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

\

把上面三個組件打包到一起進行協作,這就是HandlerThread ,筆者轉載了一篇關於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(使用handler thread)

大多數情況下,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) 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不一致問題)

當啟動工作線程的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到24,數值越小優先級越高。Android系統還為我們提供了以下的一些預設值,我們可以通過給不同的工作線程設置不同數值的優先級來達到更細粒度的控制。

\

\

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

\

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

異步任務裡線程優先級設置源代碼如下:

\

自定義線程設置優先級,主要是在handler thread中。

\

 

 

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