Android性能優化-線程性能優化


原文鏈接:Better Performance through Threading

線程的性能

熟練使用Android上的線程可以幫助你提高應用程序的性能。 本篇文章討論了使用線程的幾個方面:使用UI或主線程; 應用程序生命周期和線程優先級之間的關系; 以及平台提供的幫助管理線程復雜性的方法。 在每一部分,本篇都描述了潛在的陷阱以及如何避免它們的策略。

主線程

當用戶啟動你的應用程序時,Android會創建一個新的 Linux process 以及一個執行線程。 這個main線程,也稱為UI線程,負責屏幕上發生的一切。 了解其工作原理可以幫助你使用主線程設計你的應用程序以獲得最佳性能。

內部細節

主線程具有非常簡單的設計:它的唯一工作就是從線程安全的工作隊列中取出並執行工作塊,直到應用程序被終止。 框架從各個地方生成一些這些工作塊。 這些地方包括與生命周期信息,用戶事件(如輸入)或來自其他應用程序和進程的事件相關聯的回調。 此外,應用程序還可以在不使用框架的情況下顯式地將工作塊加入隊列。

應用程序執行的任何代碼塊都會被綁定到一個事件回調上,例如輸入,布局填充或繪制。 當某個時間觸發一個事件時,事件發生的所在線程會將事件加入到主線程的消息隊列。 之后主線程可以處理該事件。

當發生動畫或屏幕更新時,系統試圖每16ms左右執行一個工作塊(負責繪制屏幕),以便以每秒60幀的速度平滑地渲染。 為了讓系統達到這個目標,一些操作必須發生在主線程上。 但是,當主線程的消息隊列包含太多或太耗時的任務,為了讓主線程能夠在16ms內完成工作,你應將這些任務移到工作線程中去。 如果主線程不能在16ms內完成執行的代碼塊,則用戶可能感覺到卡頓或UI響應較慢。 如果主線程阻塞大約5秒鍾,系統將顯示“(ANR)”對話框,允許用戶直接關閉應用程序。

從主線程移除多個或耗時的任務,以便它們不會干擾到平滑渲染和對用戶輸入的快速響應,是你在應用程序中采用線程的最大原因。

線程和UI對象的引用

按照設計,Android UI對象不是線程安全的。 應用程序應該在主線程上創建,使用和銷毀UI對象。 如果嘗試修改或甚至引用除主線程之外的線程中的UI對象,結果可能是異常,靜默失敗,崩潰和其他未定義的錯誤行為。

UI對象引用導致的問題可以划分為兩種:顯式引用和隱式引用。

顯示引用

許多非主線程上的任務在最后都會更新UI對象。 但是,如果某一個線程訪問視圖層級中的對象,可能會導致應用的不穩定性:如果工作線程修改了同時被任何其他線程引用的對象屬性(這里都是指UI對象),則結果是不可預測的。

假設一個應用程序在工作線程上直接引用UI對象。 這個UI對象可能包含對一個View的引用; 但在工作完成之前,該View被從視圖層次結構中刪除了。 如果該引用將View對象保留在內存中並對其設置屬性,用戶並不會看到此對象,因為一旦對象的引用消失,應用程序就會刪除該對象。

再舉另一個例子,View對象(被工作線程引用)持有包含它們的Activity的引用。 如果該Activity被銷毀了,但仍有一個工作的線程直接或間接引用它 - 垃圾收集器將不會回收Activity,直到該工作線程執行完成。

在某些Activity生命周期事件(如屏幕旋轉)發生時,某些線程工作可能正在運行。 系統將無法執行垃圾回收,直到正在進行的工作完成。 因此,在內存中可能會有兩個Activity對象,直到垃圾回收發生。

考慮到以上場景,我們建議你的應用程序的工作線程中不應該包含對UI對象的顯式引用。 避免此類引用可幫助你避免這些類型的內存泄漏,同時避免線程競爭。

在所有情況下,應用程序應該只在主線程上更新UI對象。 如果有多個任務希望更新實際的UI,你應該制定一個策略,允許多個線程交互,最終將結果返回到主線程。

隱式引用

在以下代碼片段中可以看到帶有線程對象代碼的常見設計缺陷:

public class MainActivity extends Activity {
  // …...
  public class MyAsyncTask extends AsyncTask   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

這段代碼的缺陷是將線程對象MyAsyncTask聲明為一些Activity的內部類。 這種聲明創建一個對Activity對象隱式引用。 因此,該對象持有對Activity的引用,直到線程工作完成,這樣會導致所引用的Activity延遲銷毀。 這種延遲會給內存帶來更大的壓力。

解決該問題的直接解決方案是在自己的文件中定義重載類實例,從而移除對Activity的隱式引用。

另一個解決方案是將AsyncTask聲明為靜態內部類。 這樣做也可以消除隱式引用問題,因為靜態內部類與普通內部類不同:普通內部類實例需要外部類的實例才可以實例化,並且可以直接訪問其包含的方法和字段。 相比之下,靜態內部類不需要引用外部類實例,因此它不包含對外部類成員的引用。

public class MainActivity extends Activity {
  // …...
  Static public class MyAsyncTask extends AsyncTask   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

線程和應用程序以及Activity的生命周期

應用程序生命周期會對應用程序中線程的工作產生影響。 在Activity被銷毀后,你可能需要決定一個線程是否應該持久化。 還應該注意線程優先級和Activity是否在前台或后台運行之間的關系。

持久化的線程

線程的生命周期大於生成它們的Activity的生命周期。 不管Activity的創建或銷毀,線程繼續執行,不會被打斷。 在一些情況下,這種持久性是不期望的。

考慮一種情況,某個Activity發起了一組線程工作任務,但在工作線程執行完之前該Activity被銷毀了。 應用程序應該如何處理那些還在執行的任務?

如果這些任務將來會去更新不再存在的UI,那么這些任務就不應該繼續工作。例如,如果該任務是從數據庫加載用戶信息並更新視圖,那么該線程就是不需要的。

相比之下,如果任務組不是完全和UI相關的,還是很有用的。例如,任務組可能在等待下載圖片,並將其緩存到磁盤,然后去更新相關的View對象。盡管View對象不再存在,下載和緩存圖像的行為仍然是有幫助的,因為用戶有可能還會回到這個被銷毀的Activity。

手動管理所有線程對象的生命周期可能非常復雜。如果你不能正確地管理它們,你的應用程序可能會遭受內存競爭和性能問題。 Loaders 是解決這個問題的一種方案。 Loaders 有助於異步加載數據,當configuration變化時仍舊會持久化信息。

線程的優先級

進程和應用生命周期中所述,應用程序線程接收的優先級部分取決於應用在其生命周期所處的階段。 在應用程序中創建和管理線程時,設置其優先級很重要,這樣可以讓線程在正確的時間獲得正確的優先級。 如果設置太高,你的線程可能會打斷UI線程和渲染線程,導致你的應用程序丟幀。 如果設置太低,可能會導致你的異步任務(如圖像加載)比它們實際需要的慢。

每次你創建一個線程,你應該調用 setThreadPriority()方法。 系統的線程調度器程會優先選擇優先級較高的線程,並根據需要權衡這些優先級,最終完成所有的工作。 通常,前台組線程大約占用來設備總執行時間的95%,而后台組大約占5%。

系統也會通過Process類為每個線程分配其自己的優先級值。

默認情況下,系統將線程的優先級設置為與創建它的線程相同的優先級和組成員資格。 但是,你可以通過使用setThreadPriority()明確調整線程優先級。

Process類通過提供一組常量來幫助你降低分配優先級的復雜性,你可以使用這些常量來設置線程優先級。 例如,THREAD_PRIORITY_DEFAULT 表示線程的默認值。 對於不那么緊急執行的工作線程,你應將其優先級設置為THREAD_PRIORITY_BACKGROUND 。

你也可以使用 THREAD_PRIORITY_LESS_FAVORABLE 和 THREAD_PRIORITY_MORE_FAVORABLE 常量作為增量值來設定相對優先順序。 所有這些枚舉狀態和修飾符的列表出現在THREAD_PRIORITY_AUDIO 類的參考文檔中。 有關管理線程的更多信息,請參閱有 Thread and Process 的參考文檔。

https://developer.android.com/reference/android/os/Process.html#THREAD_PRIORITY_AUDIO

線程的幫助類

框架為線程提供了和java相同的類和原始類,例如Thread 和Runnable 類。 為了幫助減少與開發Android線程應用程序相關的門檻,框架提供了一組幫助類。 每個助手類在性能上都有一些細微差別,以便去處理特定的線程問題。 對錯誤的場景使用了錯誤的類可能會導致性能問題。

AsyncTask 類

AsyncTask類是一個簡單,有用的原始類,可以幫你快速將工作從主線程移動到工作線程。 例如,輸入事件可能會觸發需要加載bitmap的UI更新。AsyncTask對象可以將bitmap加載和解碼任務放到備用線程; 一旦處理完成,AsyncTask對象會返回到主線程上去更新UI。

當使用AsyncTask時,有幾個重要的性能方面的問題要記住。 首先,默認情況下,應用程序將其創建的所有AsyncTask對象推送到單個線程中。 因此,它們以串行方式執行,和主線程類似,特別耗時的工作組會阻塞隊列。 因此,我們建議你只使用AsyncTask處理持續時間短於5ms的任務。

AsyncTask對象也會導致常見的隱式引用問題。 而且,AsyncTask對象也存在顯式引用的風險,但通常這種問題比較容易解決。 例如,AsyncTask可能需要對UI對象的引用,以便在AsyncTask回調到主線程時更新UI對象。 在這種情況下,可以使用WeakReference存儲對所需UI對象的引用,在AsyncTask回調到主線程時先訪問一次該UI對象。 但你需要注意,持有某個對象的WeakReference並不會使該對象變為線程安全的; WeakReference只提供了一個處理顯式引用和垃圾回收問題的方法。

HandlerThread 類

雖然AsyncTask很有用,但對於你的線程問題,它可能不會總是正確的解決方案。相反,你可能需要一種更傳統的方法來在長時間運行的線程上執行一個工作塊,並且有一些能力來手動管理該工作流。

我們考慮從Camera對象獲取預覽幀的場景。當你注冊了相機預覽事件,將會在onPreviewFrame()回調中收到它們,該回調會在調用它的事件線程上觸發。如果這個回調在UI線程上觸發,處理大量像素數組的任務將干擾渲染和事件處理工作。AsyncTask也會有同樣的問題,AsyncTask會串行地執行任務,容易受阻塞(這個高版本已經使用線程池了)。

這種情況使用HandlerThread更合適:HandlerThread實際上是一個長時間運行的線程,它從隊列中抓取工作,並對其進行操作。在這個例子中,當你的應用程序將Camera.open()命令委托給HandlerThread上的一個工作塊時,相關的onPreviewFrame()回調會落在HandlerThread上,而不是UI或AsyncTask線程。所以,如果你要對像素進行長時間的操作,這可能是一個更好的解決方案。

當你的應用程序使用HandlerThread創建一個線程時,不要忘記根據工作類型設置線程的優先級。 記住,CPU只能並行處理少數線程。當所有其他線程都在爭取資源時, 設置優先級有助於系統知道如何正確的調度這項工作。

ThreadPoolExecutor 類

有些類型的工作是高度並行,分布式的。例如,為8百萬像素圖像的每個8×8塊計算濾波。創建這種量級的工作,AsyncTask和HandlerThread都不合適。 AsyncTask的單線程性質將所有線程池工作轉換為線性系統。另一方面,使用HandlerThread類將需要程序員手動管理一組線程之間的負載平衡。

這種情況,使用ThreadPoolExecutor類來處理會更容易。該類可以管理一組線程的創建,優先級設置,並權衡分配到這些線程的任務如何處理。隨着工作負載增加或減少,該類會自動啟動或銷毀線程來適應工作負載。

此類還可以幫助你的應用程序創建最佳線程數。當在構造一個ThreadPoolExecutor對象時,可以設置最小和最大線程數。隨着ThreadPoolExecutor的負載增加,該類將考慮初始化的最小和最大線程數,並考慮待處理的工作量。基於這些因素,ThreadPoolExecutor決定在任何給定時間應該有多少線程存活。

你應該創建多少線程?

雖然從軟件層面來看,你的代碼有能力創建數百個線程,但這樣做可能會造成性能問題。 CPU只有並行處理少量線程的能力;以上提到的都會遇到優先級和調度問題。因此,只創建與你的工作負載需要的線程是很重要的。

實際上,許多因素都會對優先級和調度有影響,但你可以選擇一個值(比如初始值設為4),並通過 Systrace 進行測試。通過試錯的方式來確定可以使用而又不會產生問題的最小線程數。

你需要考慮創建多少線程的另一個原因是線程不是免費的:它們占用內存。每個線程最少消耗64k內存。如果設備上安裝了許多應用,該值就會快速添加,特別是在調用棧顯著增長的情況下。

許多系統進程和第三方庫經常調度自己的線程池。如果你的應用程序可以重用現有的線程池,則此重用能夠減少內存和處理資源的競爭來幫助提高性能。


來源:http://www.lightskystreet.com/2016/10/18/android-optimize-thread/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM