熟練使用Android上的線程可以幫助你提高應用程序的性能。本文討論使用線程的幾個方面:使用UI或主線程、應用程序生命周期與線程優先級之間的關系、以及平臺提供的幫助管理線程復雜性的方法。
主線程
當用戶啟動你的應用程序時,Android會創建一個新的Linux進程以及一個執行線程。這個主線程, 也被稱為UI線程
,負責屏幕上發生的所有事情。了解它如何工作可以幫助您設計您的應用程序使用主線程以獲得最佳性能。
主線程
的設計非常簡單:它唯一的工作就是從線程安全的工作隊列中取出并執行工作塊,直到其應用程序終止。該框架從各個地方生成了這些工作的一部分。這些地方包括與生命周期信息相關的回調,用戶事件(如輸入)或來自其他應用程序和進程的事件。此外,應用程序可以自己明確排隊,而無需使用框架。
應用程序 幾乎執行任何代碼塊都與事件回調有關,例如輸入,布局膨脹或繪制。當事件觸發事件時,事件發生的線程將事件推出自身,并進入主線程的消息隊列。主線程可以為事件提供服務。
當動畫或畫面更新發生時,系統每隔16ms
左右嘗試執行一個工作塊(負責繪制畫面),以便以每秒60幀的速度平滑地進行渲染。為了達到這個目標,UI / View
層次結構必須在主線程上更新。但是,當主線程的消息傳遞隊列包含的任務太多或太長時,主線程無法完成足夠快的更新時,應用程序應將此工作移至工作線程。如果主線程無法在16ms
內完成工作塊,用戶可能會觀察到拖尾,或者對輸入的UI響應性不足。如果主線程阻塞大約五秒鐘
,則系統顯示應用程序不響應 (ANR
)對話框,允許用戶直接關閉應用程序。
從主線程中移動大量或長時間的任務,以避免影響用戶輸入的平滑呈現和快速響應,是您在應用中采用線程的最大原因。
線程和UI對象引用
按照設計,Android視圖對象不是線程安全的。預計應用程序將在主線程上創建,使用和銷毀UI對象。如果你嘗試修改甚至引用主線程以外的線程中的UI對象,則結果可能是異常,將無提示失敗、崩潰以及其他未定義的錯誤行為。
引用的問題分為兩個不同的類別:顯式引用
和隱式引用
。
顯式引用
非主線程上的許多任務都有更新UI對象的最終目標。但是,如果其中一個線程訪問視圖層次結構中的對象,則可能導致應用程序不穩定:如果工作線程在任何其他線程正在引用該對象的同時更改該對象的屬性,則結果是未定義的。
例如,考慮在工作線程上保存對UI對象的直接引用的應用程序。工作線程上的對象可能包含對a的引用 View; 但在工作完成之前,View將從視圖層次結構中移除。當這兩個動作同時發生時,引用將View對象保存在內存中并在其上設置屬性。但是,用戶從不會看到這個對象,并且一旦對象的引用消失,該應用程序就會刪除該對象。
在另一個例子中,View對象包含對擁有它們的活動的引用。如果這個活動被破壞了,但是仍有一個直接或間接引用它的線程塊,那么垃圾收集器將不會收集這個活動,直到這個工作塊完成執行。
在發生線程工作的情況下,如果發生某個活動生命周期事件(如屏幕旋轉),此情形可能會導致出現問題。系統將無法執行垃圾收集,直到正在進行的工作完成。因此,Activity在垃圾收集發生之前,內存中可能有兩個對象。
通過這樣的場景,我們建議您的應用程序不要在線程工作任務中包含對UI對象的顯式引用。避免這種引用可以幫助您避免這些類型的內存泄漏,同時避免線程爭用。
在所有情況下,您的應用程序只應更新主線程上的UI對象。這意味著您應該制定一個協商策略,允許多個線程將工作交流回主線程,主線程將更新實際UI對象的工作作為最高活動或片段。
隱式引用
在下面的代碼片段中可以看到一個帶有線程對象的常見代碼設計缺陷:
public class MainActivity extends Activity {
// …...
public class MyAsyncTask extends AsyncTask<Void, Void, String> {
@Override protected String doInBackground(Void... params) {...}
@Override protected void onPostExecute(String result) {...}
}
}
這段代碼中的缺陷是代碼將線程對象聲明MyAsyncTask
為一些活動的非靜態內部類
。這個聲明創建了一個對包含Activity實例的隱式引用
。因此,該對象包含對該活動的引用,直到線程工作完成,從而導致引用活動的銷毀延遲。這種延遲反過來又會增加內存。
直接解決這個問題的方法是將你的重載的類實例定義為靜態類
,或者在它們自己的文件中去掉隱式引用。
另一個解決方案是將AsyncTask對象聲明為靜態嵌套類
。這樣做可以消除隱式引用問題,因為靜態嵌套類不同于內部類:內部類的實例需要實例化外部類的實例,并且可以直接訪問它的封閉方法和字段實例。相比之下,靜態嵌套類不需要引用包含類的實例,因此它不包含對外部類成員的引用。
public class MainActivity extends Activity {
// …...
static public class MyAsyncTask extends AsyncTask<Void, Void, String> {
@Override protected String doInBackground(Void... params) {...}
@Override protected void onPostExecute(String result) {...}
}
}
線程和應用程序以及活動生命周期
應用程序生命周期可以影響線程在您的應用程序中的工作方式。你可能需要決定一個線程應該或不應該在一個活動被破壞之后持續下去。你還應該了解線程優先級與活動是在前臺還是在后臺運行之間的關系。
持續執行線程
線程一直持續到產生它們的活動的生命周期。線程繼續執行,不受干擾,無論創建或破壞活動。在某些情況下,這種持久性是可取的。
考慮一種情況,其中一個活動產生一組線程化的工作塊,然后在工作線程可以執行塊之前被銷毀。應用程序應該怎樣處理正在運行的程序段?
如果塊要更新不再存在的用戶界面,則沒有理由繼續工作。例如,如果工作是從數據庫加載用戶信息,然后更新視圖,則不再需要該線程。
相比之下,工作包可能有一些不完全與用戶界面相關的好處。在這種情況下,你應該堅持這個線程。例如,數據包可能正在等待下載映像,將其緩存到磁盤,并更新關聯的 View對象。盡管該對象不再存在,但是在用戶返回被銷毀的活動的情況下,下載和緩存圖像的行為仍可能是有幫助的。
手動管理所有線程對象的生命周期響應可能變得非常復雜。如果你不正確地管理它們,你的應用程序可能會遭受內存爭用和性能問題。裝載機 是解決這個問題的方法之一。加載程序有助于異步加載數據,同時還可以通過配置更改來保存信息。
線程優先級
如“ 進程和應用程序生命周期”
中所述,應用程序線程獲得的優先級部分取決于應用程序生命周期中的應用程序的位置。在創建和管理應用程序中的線程時,重要的是設置其優先級,以便正確的線程在正確的時間獲得正確的優先級。如果設置得太高,你的線程可能會中斷UI線程和RenderThread
,導致你的應用程序丟幀。如果設置得太低,可以使你的異步任務(如圖像加載)比他們需要的慢。
每當你創建一個線程,你應該打電話 setThreadPriority()
。系統的線程調度器優先考慮高優先級的線程,平衡這些優先級,最終完成所有工作。一般來說,前臺組中的線程占設備總執行時間的95%左右,而后臺組大約占5%。
系統還使用Process該類為每個線程分配自己的優先級值 。
默認情況下,系統將線程的優先級設置為與產卵線程相同的優先級和組成員資格。但是,您的應用程序可以使用明確調整線程優先級
setThreadPriority()
。
你的應用程序應該將線程的優先級設置為THREAD_PRIORITY_BACKGROUND
為執行不太緊急工作的線程。
你的應用程序可以使用THREAD_PRIORITY_LESS_FAVORABLE
和THREAD_PRIORITY_MORE_FAVORABLE
常量作為增量來設置相對優先級。有關線程優先級的列表,請參閱類中的THREAD_PRIORITY
常量Process。
線程的helper類
Fragment提供了相同的Java類和基本類型,以方便線程,比如Thread
,Runnable
和Executors
類。為了幫助減少與為Android開發線程應用程序相關的認知負載,框架提供了一系列可以幫助開發的幫助程序,例如AsyncTaskLoader
和AsyncTask
。每個輔助類都有一組特定的性能細微差別,這些細微差別使得它們對于特定的線程問題子集是唯一的。對錯誤的情況使用錯誤的類可能會導致性能問題。
AsyncTask類
本AsyncTask類是需要快速從主線程移動工作到工作線程應用程序的簡單,實用的原始。例如,輸入事件可能觸發需要用加載的位圖更新UI。一個AsyncTask 對象可以卸載位圖加載和解碼到另一個線程; 一旦處理完成,AsyncTask對象可以管理接收主線程上的工作以更新UI。
使用時AsyncTask,要記住一些重要的性能方面。首先,默認情況下,一個應用程序將AsyncTask 其創建的所有對象推送到單個線程中。因此,它們以串行方式
執行,并且與主線程一樣,特別長的工作包可以阻塞隊列。因此,我們建議您僅AsyncTask處理短于5ms的工作項目。
AsyncTask對象也是隱式引用問題的最常見的。 AsyncTask對象也存在與明確引用有關的風險,但這些有時候更容易解決。例如,AsyncTask 為了正確地更新UI對象,可能需要對UI對象的引用,一旦AsyncTask在主線程上執行其回調。在這種情況下,您可以使用一個WeakReference
來存儲對所需UI對象的引用,并AsyncTask在主線程上運行時訪問該對象 。要清楚,持有一個WeakReference
對象不會使對象線程安全; 在 WeakReference
僅提供處理與明確提到和垃圾收集問題的方法。
HandlerThread類
雖然一個AsyncTask 是有用的, 它可能并不總是正確的解決你的線程問題。相反,您可能需要更傳統的方法來在較長時間運行的線程上執行一個工作塊,以及手動管理該工作流的能力。
考慮從Camera對象獲取PreviewFrame的常見挑戰 。當你注冊攝像頭預覽幀時,你會在onPreviewFrame()
調中接收到這些回調,該回調將在調用它的事件線程上調用。如果在UI線程上調用此回調函數,則處理巨大像素數組的任務將干擾渲染和事件處理工作。同樣的問題也適用于AsyncTask連續執行作業,這容易被阻塞。
這是一個處理程序線程適當的情況:處理程序線程實際上是一個長時間運行的線程,它從隊列中抓取工作,并對其進行操作。在這個例子中,當你的應用程序把這個Camera.open()
命令委托 給處理程序線程的一個工作塊時,相關的onPreviewFrame()
回調就落在處理程序線程上,而不是UI或AsyncTask 線程上。所以,如果你要在像素上做長時間的工作,這可能是一個更好的解決方案。
當你的應用程序創建一個使用的線程時HandlerThread,不要忘記 根據它所做的工作類型來設置線程的 優先級。請記住,CPU只能并行處理少量的線程。設置優先級有助于系統知道正確的方式來安排這項工作,當所有其他線程爭取注意。
ThreadPoolExecutor類
有一些類型的工作可以降低到高度并行的分布式任務。例如,一個這樣的任務是為一個8兆像素圖像的每個8×8塊計算濾波器。這個工作包的數量很大,而且不是合適的類別。單線程本質將把所有的線程化工作轉化為一個線性系統。另一方面,使用這個類會要求程序員手動管理一組線程之間的負載平衡。
ThreadPoolExecutor
是一個輔助類,使這個過程更容易。這個類管理著一組線程的創建,設置它們的優先級,并管理這些線程之間的工作分配方式。隨著工作量的增加或減少,這個類會加速或破壞更多的線程以適應工作負載。
這個類也可以幫助你的應用產生最佳的線程數量。當它構造一個ThreadPoolExecutor
對象時,應用程序設置最小和最大線程數。由于ThreadPoolExecutor
增加的工作量 ,班級將考慮初始化的最小和最大線程數,并考慮待處理的工作量?;谶@些因素,ThreadPoolExecutor
決定在任何給定時間應該有多少線程活著。
你應該創建多少個線程?
盡管從軟件級別來看,你的代碼有能力創建數百個線程,但這樣做會造成性能問題。你的應用程序與后臺服務,渲染器,音頻引擎,網絡等共享有限的CPU資源。CPU實際上只能并行處理少量的線程,上面的所有內容都會遇到 優先級和調度問題。因此,只需創建與您的工作負載所需的線程數量就很重要。
實際上,有很多變量對此負責,但是選擇一個值(比如4,對于初學者),并且使用Systrace進行測試與 其他方法一樣可靠。你可以使用反復試驗來發現可以使用的最少線程數量,而不會遇到問題。
決定有多少線程的另一個考慮因素是線程不是免費的:它們占用內存。每個線程最少需要64k的內存。這在設備上安裝的許多應用程序中快速增加,尤其是在調用堆棧顯著增長的情況下。
許多系統進程和第三方庫經常會啟動自己的線程池。如果你的應用程序可以重復使用現有的線程池,則這種重用可以通過減少內存和處理資源的爭用來幫助提高性能。