【官網翻譯】性能篇(二)經過線程提升性能

前言html

       本文翻譯自Android開發者文檔中的一篇官方文檔,用於介紹如何經過正確使用線程來提高應用性能(Better performance through threading)。java

       中國版官網原文地址爲:https://developer.android.google.cn/topic/performance/threadsandroid

       路徑爲:Android Developers > Docs > 指南 > Best practies > Better performance through threading程序員

 

正文數據庫

       在Android中熟練使用線程可以幫助您提高您應用的性能。本頁將會討論用線程工做的幾個方面:使用UI線程或主線程工做;應用的生命週期和線程優先級之間的關係,以及平臺提供的用於管理線程複雜度的方法。本頁將描述這其中任何一個方面中可能的陷阱和避免它們的策略。緩存

 

主線程安全

       當用戶啓動您的應用時,Android會建立一個攜帶執行線程的【Linux進程】。這個主線程,也做爲UI線程被熟知,對屏幕上所發生的一切負責。理解它是如何工做的能夠幫助您使用主線程來設計您的應用,以得到最佳可能的性能。網絡

       內幕架構

       主線程有一個很是簡單的設計:它惟一的工做就是獲取並執行來自於線程安全工做隊列的工做塊,直到它的應用終止。框架從不一樣的地方生成了這些工做塊中的一部分。這些地方包括與生命週期信息相關聯的回調,輸入等用戶事件,或者來自於其餘應用和進程的事件。除此以外,應用能夠在不使用框架的狀況下,經過本身來顯示地將這些塊加入到隊列(線程安全工做隊列:譯者注)中。框架

       幾乎任何一個你的應用執行的代碼塊都被綁定到一個事件回調,好比輸入,佈局填充,或者繪製。當某事物觸發了一個事件,這個事件所發生的線程會把該事件推出,而且推入到主線程消息隊列中。而後這個主線程會服務該事件。

       當動畫事件或者屏幕更新發生了,爲了以60幀每秒的頻率平滑地渲染,系統會嘗試每16毫秒執行一個工做塊(該工做塊用於負責繪製屏幕)。爲了讓系統到達這個目標,UI/View層級必須在主線程中更新。但是,當主線程消息隊列包含了太多或太長的任務以致於 主線程沒法足夠快地完成更新時,應用應該把這些工做移到工做線程中。若是主線程沒法在16毫秒之內沒法完成執行工做塊,用戶可能會觀察到鉤住、滯後或者對輸入缺少UI響應。若是主線程阻塞了大約5秒時間,系統會顯示一個「應用程序沒有響應(ANR)」對話框,以容許用戶直接關閉這個應用。

       從主線程中移除大量或太長時間的任務,這樣的話它們就不會干擾平滑的渲染和對用戶輸入的響應,這是你在應用中採用線程的最大緣由。

 

線程和UI對象引用

       經過設計,Android View對象不是線程安全的。應用所預期的是建立、使用以及銷燬UI對象,都在主線程中。若是你嘗試在其它線程而不是主線程修改甚至引用一個UI對象,結果多是異常,無聲故障,崩潰,以及其它未定義的錯誤行爲。

       引用問題被分爲兩類:顯示引用和隱式引用。

       顯示引用

       許多在非主線程上的任務都有一個更新UI對象的最終目標。但是,若是這些線程在View層級上訪問對象,可能會致使應用不穩定:若是一個工做線程改變了一個對象的屬性,而與此同時其它線程正在引用這個對象,其結果是未知的。

       例如,設想一個在工做線程上持有UI對象直接引用的應用。在工做線程上的對象可能包含了一個對View的引用;可是在工做完成以前,這個View被從view層級中移除了。當這兩個動做同時發生時,引用將View對象保留在了內存中而且在它上面設置了屬性。但是,用戶從未看到過這個對象,而且一旦對它引用消失,應用就會刪除這個對象。

       舉另一個例子,View對象包含了對activity的引用,而這個actvity又擁有這些View對象。若是那個activity銷燬了,可是仍然存在一個引用它的線程工做塊——直接或間接地——垃圾收集器將不會收集activity,直到那個工做塊執行結束。

       當某個諸如屏幕旋轉等activity生命週期事件發生時,線程工做可能正在運行,在這種情形下可能會致使一個問題。系統將沒法執行垃圾收集,直到正在運行的工做完成。結果,可能會有兩個Activity對象在內存中,直到可以發生垃圾收集。

       在像這樣的場景下,咱們建議您的應用在工線程工做任務中不要包含對UI對象的顯示引用。避免這樣的應用會幫助您避免這些類型的內存泄漏,同時避免線程競爭。

       在全部情形下,您的應用應該只在主線程更新UI對象。這意味着您應該制定一個協商策略,以容許多個線程將工做傳遞迴主線程,主線程經過更新實際的UI對象來執行最頂層的activity或fragment。

       隱式引用

       一個經常使用的使用線程對象的代碼設計瑕疵可能如如下代碼片斷所看到的:

1 //for java
2 public class MainActivity extends Activity {
3   // ...
4   public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
5     @Override protected String doInBackground(Void... params) {...}
6     @Override protected void onPostExecute(String result) {...}
7   }
8 }

 

1 //for kotlin
2 class MainActivity : Activity() {
3     // ...
4     class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
5         override fun doInBackground(vararg params: Unit): String {...}
6         override fun onPostExecute(result: String) {...}
7     }
8 }

 

       這個片斷中的缺陷是,這段代碼聲明瞭線程對象MyAsyncTask爲一個非靜態Activity內部類(或者Kotlin中的內部類)。這個聲明建立了一個封裝Activity實例的隱式引用。所以,這個對象包含了一個activity引用,直到線程工做完成,在銷燬這個被引用的activity時致使了一個延遲。反過來,這個延遲給內存施加了更多的壓力。

       解決這個問題最直接的途徑是定義您的重載類實例爲靜態類,或者在它們本身的文件中定義,這樣以移除隱式引用。

       另一種解決途徑是聲明這個AsyncTask對象爲一個靜態嵌套類(或者在Kotlin中移除內部修飾符)。這樣作消除了隱式引用問題,由於靜態嵌套類的方式和內部類有所不一樣:內部類的實例須要外部類實例進行實例化,並直接訪問該封裝實例的方法和字段。相比之下,一個靜態的嵌套類不須要引用封裝類的實例,因此它不包含對外部類成員的引用。

1 //for java
2 public class MainActivity extends Activity {
3   // ...
4   static public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
5     @Override protected String doInBackground(Void... params) {...}
6     @Override protected void onPostExecute(String result) {...}
7   }
8 }

 

1 //for Kotlin
2 class MainActivity : Activity() {
3     // ...
4     class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
5         override fun doInBackground(vararg params: Unit): String {...}
6         override fun onPostExecute(result: String) {...}
7     }
8 }

 

線程和應用activity生命週期

       應用生命週期能夠影響到在您的應用中線程是如何工做。您可能須要決定在activity銷燬後線程是否應該繼續存在。您還應該瞭解線程優先級和activity是在前臺仍是在後臺運行之間的關係。

       存留線程

       線程伴隨着產生它們的activity的一輩子而一直存在。不管activity建立仍是銷燬,線程都繼續運行,不會中斷。在有些狀況下,這種留存是可取的。

       考慮一種狀況,activity生成了一組線程工做塊,而且隨後在工做線程能夠執行這些塊以前銷燬了。應用應該如何處理這些正在運行的塊呢?

       若是這些塊要去更新再也不存在的UI,那麼就沒有任何理由讓該工做繼續。例如,若是該工做用於加載來自數據庫的用戶信息,而後更新視圖,那麼這個線程就是沒必要要的。

       相比之下,工做包可能有一些和UI不徹底相關的好處。在這種狀況下,您應該存留這個線程。例如,這些包可能正在等待下載一張圖片,緩存到磁盤,以及更新這個相關的View對象。雖然這個對象再也不存在了,可是下載和緩存圖片的行動可能仍然是有幫助的,萬一用戶返回到這個被銷燬的activity呢。

       手動爲全部線程對象管理生命週期響應可能變得異常複雜。若是您沒有正確管理它們,您的應用可能忍受內存競爭和性能問題。將ViewModel和LiveData結合使用,能夠在數據更改時,容許您加載數據,並獲得通知,而不用擔憂生命週期。ViewModel對象是解決這個問題的一種途徑。ViewModels是在配置的更改中被維護的,這提供了一種簡單的方法來保留您的視圖數據。關於ViewModels的更多信息,請查看【ViewModel指導】,以及學習更多關於LiveData的知識,請查看【LiveData指導】。若是您還想了解更多關於應用架構的信息,請閱讀【應用架構指導

       線程優先級

       正如在【進程和應用生命週期】中描述的那樣,您應用的線程所接收到的優先級部分依賴於應用所處的應用生命週期。當您建立和管理您應用中的線程時,設置它們的優先級從而讓正確的線程在正確的時間獲取正確的優先級是一件重要的事。若是設置得過高,您的線程可能會中斷UI線程和RenderThread,這會致使您的應用丟幀。若是設置得過低,您會使得您的同步任務(好比圖片加載)比它們須要的慢。

       任什麼時候刻您建立線程,您應該調用setThreadPriority()。系統的線程調度器優先選擇高優先級的線程,讓優先級和最終完成全部工做的須要相平衡。通常來講,前臺組線程獲取了大約95%的設備總執行時間,然然後臺組大約只獲取約5%。

       系統也使用Process類給每個線程分配它們本身的優先值。

       默認狀況下,系統給線程優先級設置爲和孵化線程相同的優先級和組成員身份。可是,您的應用能夠經過使用setThreadPriority()顯示地調整線程優先級。

       Process類經過提供一組常量來幫助下降分配優先級值時的複雜度,您的應用可使用這組常量來設置線程優先級。例如,THREAD_PRIORITY_DEFAULT表明了線程的默認值。您的應用應該把那些正在執行的非緊急工做的線程的線程優先級設置爲THREAD_PRIORITY_BACKGROUND。

       你的應用可使用THREAD_PRIORITY_LESS_FAVORABLE 和 THREAD_PRIORITY_MORE_FAVORABLE常量做爲增量來設置相對優先級。對於線程優先級列表,能夠在Process類中查看【THREAD_PRIORITY】常量。

       對於更多管理線程方面的信息,請查看關於【Thread】和【Process】類的引用文檔。

        

線程幫助類

       框架提供了相同的Java類和基礎來幫助使用線程,好比Thread,Runnable以及Executors類。爲了幫助下降和正在開發的Android線程應用相關的負載,框架提供了一組能夠輔助開發的助手,好比AsyncTaskLoader和AsyncTask。每一個幫助類都有一組特定的性能細微差異,使得它們對於線程問題的特定子集來講是獨一無二的。在錯誤的場景使用錯誤的類會引發性能問題。

       AsyncTask類

       對於那些須要快速將任務從主線程轉移到工做線程的應用而言,AsyncTask類是一個簡單的,有用的基類。例如,輸入事件可能會觸發使用加載的位圖來更新UI的需求。AsyncTask對象可以將位圖加載和解碼卸載到備用線程;一旦處理完成,AsyncTask對象能夠管理接收返回到主線程的工做來更新UI。

       當使用AsyncTask時,有一些重要的性能方面須要考慮。首先,默認狀況下,應用會把它所建立的全部AsyncTask對象推入一個單線程。因此,它們以串行方式執行,而且和主線程同樣,特別長的工做包會阻塞隊列。因此,我建議您只使用AsyncTask處理時長少於5ms的工做項。

       AsyncTask對象也是隱式引用問題最廣泛的罪魁禍首。AsyncTask對象也會產生和顯式引用相關的風險,但有時更容易解決這些問題。例如,一旦AsyncTask在主線程上執行它的回調,爲了正確地更新UI對象,AsyncTask可能須要引用UI對象。在這種狀況下,您可使用WeakReference來存儲對所需的UI對象引用,以及一旦AsyncTask在主線程上運行,能夠訪問該對象。須要清楚的是,持有對一個對象弱引用,不會讓這個對象線程安全;弱引用僅僅提供了一種方法處理顯式引用和垃圾收集問題。

       HandlerThread類

       雖然AsyncTask可用,但它可能並不老是您線程問題正確的解決途徑。相反,您可能須要一個更加傳統的途徑來執行長時間運行的線程上的工做塊,以及一些手動管理那些工做流的能力。

       經過從您的Camera對象中獲取預覽幀,考慮一個常見的挑戰。當您註冊了Camera預覽幀,您從onPreviewFrame()回調中收到它們,該回調被調用它的工做線程所調用。若是該回調在UI線程中被調用,處理巨大像素陣列的任務將會被渲染和事件進程工做所幹擾。一樣的問題也適用於AsyncTask,它也串行執行工做而且很容易阻塞。

       這是一種handler 線程可能適用的場景:handler線程其實是一個長時間運行的線程,它從隊列中獲取任務而且在它上面操做。在這個例子中,當您的應用委派Camera.open()命令給handler線程上的工做塊時,相關聯的onPreviewFrame()回調降臨到handler線程,而不是UI或AsyncTask線程。因此,若是您即將處理長時間運行的像素上的工做,對您來講這多是一個更好的解決途徑。

       當您的應用使用HandlerThread建立一個線程,不要忘記在它正在處理的這類工做的基礎上設置這個線程的優先級。切記,CPU只能並行處理少許的線程。當全部其餘線程在爭奪關注時,設置優先級會幫助系統知道正確的方法調度這項任務。

       ThreadPoolExecutor類

       有一些明確類型的工做能夠被簡化爲高度並行的分佈式任務。例如,其中一項任務就是爲每個8百萬像素圖片的8x8塊計算一個過濾器。因爲建立了大量的工做包,AsyncTask和HandlerThread都不是合適的類。AsyncTask的單線程特性會把全部的線程池工做轉變爲一個線性系統。另外一方面,使用HandlerThread類須要程序員手動管理一組線程之間的負載平衡。

       ThreadPoolExector類是一個幫助類,用於讓進程更簡單。這個類用於管理一組線程的建立,設置他們的優先級,以及管理這些線程之間如何分配工做。當工做量增長了或者減小了,該類建立或者銷燬更多的線程以調整工做量。

       這個類也幫助您的應用生成適宜數量的線程。當構建一個ThreadPoolExecutor對象時,應用設置了一個最小和最大數量的線程。當給予ThreadPoolExecutor的工做量增長時,該類將會考慮初始化的最小和最大的線程數量,以及考慮即將要進行的工做的數據。基於這些因素,ThreadPoolExecutor決定了在任意給定的時間點多少線程應該是存活的。

       您應該建立多少個線程?

       雖然從軟件層面上來看,您的代碼有能力建立幾百個線程,可是這樣作會建立性能問題。您的應用和後臺service、渲染器、音頻引擎、網絡以及更多功能共享有限的CPU資源。CPU確實只有能力並行處理少許的線程;以上的全部一切都會產生優先級和調度問題。所以,根據您工做量的須要建立線程的數量是很重要的。

       實際上,有不少變量形成這個緣由,可是選擇一個值(好比4,做爲初始值),而且使用Systrace來測試是一個和其它方案同樣穩定的策略。您可使用反覆試驗的方法找到您可使用的最小線程數量,而不會產生問題。

       另一個決定擁有多少線程的考慮就是線程不是「免費」的:它們佔用內存。每個線程花費了至少64k內存。這經過安裝在設備上的應用很快累積起來,尤爲是在調用棧顯著地增加的情形下。

       許多系統進程和第三方庫常常建立它們本身的線程池。若是您的應用可以重複使用一個存在的線程池,那麼這個重複使用可能經過下降內存競爭和進程資源對性能有所幫助。

 

結語

       本文最大限度保持原文的意思,因爲筆者水平有限,如有翻譯不許確或不穩當的地方,請指正,謝謝!

相關文章
相關標籤/搜索