Android開發(fā)套路# 線程和軟件開發(fā)

熟練使用Android上的線程可以幫助你提高應用程序的性能。本文討論使用線程的幾個方面:使用UI或主線程、應用程序生命周期與線程優(yōu)先級之間的關(guān)系、以及平臺提供的幫助管理線程復雜性的方法。

主線程

當用戶啟動你的應用程序時,Android會創(chuàng)建一個新的Linux進程以及一個執(zhí)行線程。這個主線程, 也被稱為UI線程,負責屏幕上發(fā)生的所有事情。了解它如何工作可以幫助您設(shè)計您的應用程序使用主線程以獲得最佳性能。

主線程的設(shè)計非常簡單:它唯一的工作就是從線程安全的工作隊列中取出并執(zhí)行工作塊,直到其應用程序終止。該框架從各個地方生成了這些工作的一部分。這些地方包括與生命周期信息相關(guān)的回調(diào),用戶事件(如輸入)或來自其他應用程序和進程的事件。此外,應用程序可以自己明確排隊,而無需使用框架。

應用程序 幾乎執(zhí)行任何代碼塊都與事件回調(diào)有關(guān),例如輸入,布局膨脹或繪制。當事件觸發(fā)事件時,事件發(fā)生的線程將事件推出自身,并進入主線程的消息隊列。主線程可以為事件提供服務(wù)。

當動畫或畫面更新發(fā)生時,系統(tǒng)每隔16ms左右嘗試執(zhí)行一個工作塊(負責繪制畫面),以便以每秒60幀的速度平滑地進行渲染。為了達到這個目標,UI / View層次結(jié)構(gòu)必須在主線程上更新。但是,當主線程的消息傳遞隊列包含的任務(wù)太多或太長時,主線程無法完成足夠快的更新時,應用程序應將此工作移至工作線程。如果主線程無法在16ms內(nèi)完成工作塊,用戶可能會觀察到拖尾,或者對輸入的UI響應性不足。如果主線程阻塞大約五秒鐘,則系統(tǒng)顯示應用程序不響應 (ANR)對話框,允許用戶直接關(guān)閉應用程序。

從主線程中移動大量或長時間的任務(wù),以避免影響用戶輸入的平滑呈現(xiàn)和快速響應,是您在應用中采用線程的最大原因。

線程和UI對象引用

按照設(shè)計,Android視圖對象不是線程安全的。預計應用程序?qū)⒃谥骶€程上創(chuàng)建,使用和銷毀UI對象。如果你嘗試修改甚至引用主線程以外的線程中的UI對象,則結(jié)果可能是異常,將無提示失敗、崩潰以及其他未定義的錯誤行為。

引用的問題分為兩個不同的類別:顯式引用隱式引用。

顯式引用

非主線程上的許多任務(wù)都有更新UI對象的最終目標。但是,如果其中一個線程訪問視圖層次結(jié)構(gòu)中的對象,則可能導致應用程序不穩(wěn)定:如果工作線程在任何其他線程正在引用該對象的同時更改該對象的屬性,則結(jié)果是未定義的。

例如,考慮在工作線程上保存對UI對象的直接引用的應用程序。工作線程上的對象可能包含對a的引用 View; 但在工作完成之前,View將從視圖層次結(jié)構(gòu)中移除。當這兩個動作同時發(fā)生時,引用將View對象保存在內(nèi)存中并在其上設(shè)置屬性。但是,用戶從不會看到這個對象,并且一旦對象的引用消失,該應用程序就會刪除該對象。

在另一個例子中,View對象包含對擁有它們的活動的引用。如果這個活動被破壞了,但是仍有一個直接或間接引用它的線程塊,那么垃圾收集器將不會收集這個活動,直到這個工作塊完成執(zhí)行。

在發(fā)生線程工作的情況下,如果發(fā)生某個活動生命周期事件(如屏幕旋轉(zhuǎn)),此情形可能會導致出現(xiàn)問題。系統(tǒng)將無法執(zhí)行垃圾收集,直到正在進行的工作完成。因此,Activity在垃圾收集發(fā)生之前,內(nèi)存中可能有兩個對象。

通過這樣的場景,我們建議您的應用程序不要在線程工作任務(wù)中包含對UI對象的顯式引用。避免這種引用可以幫助您避免這些類型的內(nèi)存泄漏,同時避免線程爭用。

在所有情況下,您的應用程序只應更新主線程上的UI對象。這意味著您應該制定一個協(xié)商策略,允許多個線程將工作交流回主線程,主線程將更新實際UI對象的工作作為最高活動或片段。

隱式引用

在下面的代碼片段中可以看到一個帶有線程對象的常見代碼設(shè)計缺陷:

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為一些活動的非靜態(tài)內(nèi)部類。這個聲明創(chuàng)建了一個對包含Activity實例的隱式引用。因此,該對象包含對該活動的引用,直到線程工作完成,從而導致引用活動的銷毀延遲。這種延遲反過來又會增加內(nèi)存。

直接解決這個問題的方法是將你的重載的類實例定義為靜態(tài)類,或者在它們自己的文件中去掉隱式引用。

另一個解決方案是將AsyncTask對象聲明為靜態(tài)嵌套類。這樣做可以消除隱式引用問題,因為靜態(tài)嵌套類不同于內(nèi)部類:內(nèi)部類的實例需要實例化外部類的實例,并且可以直接訪問它的封閉方法和字段實例。相比之下,靜態(tài)嵌套類不需要引用包含類的實例,因此它不包含對外部類成員的引用。

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) {...}
  }
}

線程和應用程序以及活動生命周期

應用程序生命周期可以影響線程在您的應用程序中的工作方式。你可能需要決定一個線程應該或不應該在一個活動被破壞之后持續(xù)下去。你還應該了解線程優(yōu)先級與活動是在前臺還是在后臺運行之間的關(guān)系。

持續(xù)執(zhí)行線程

線程一直持續(xù)到產(chǎn)生它們的活動的生命周期。線程繼續(xù)執(zhí)行,不受干擾,無論創(chuàng)建或破壞活動。在某些情況下,這種持久性是可取的。

考慮一種情況,其中一個活動產(chǎn)生一組線程化的工作塊,然后在工作線程可以執(zhí)行塊之前被銷毀。應用程序應該怎樣處理正在運行的程序段?

如果塊要更新不再存在的用戶界面,則沒有理由繼續(xù)工作。例如,如果工作是從數(shù)據(jù)庫加載用戶信息,然后更新視圖,則不再需要該線程。

相比之下,工作包可能有一些不完全與用戶界面相關(guān)的好處。在這種情況下,你應該堅持這個線程。例如,數(shù)據(jù)包可能正在等待下載映像,將其緩存到磁盤,并更新關(guān)聯(lián)的 View對象。盡管該對象不再存在,但是在用戶返回被銷毀的活動的情況下,下載和緩存圖像的行為仍可能是有幫助的。

手動管理所有線程對象的生命周期響應可能變得非常復雜。如果你不正確地管理它們,你的應用程序可能會遭受內(nèi)存爭用和性能問題。裝載機 是解決這個問題的方法之一。加載程序有助于異步加載數(shù)據(jù),同時還可以通過配置更改來保存信息。

線程優(yōu)先級

“ 進程和應用程序生命周期”中所述,應用程序線程獲得的優(yōu)先級部分取決于應用程序生命周期中的應用程序的位置。在創(chuàng)建和管理應用程序中的線程時,重要的是設(shè)置其優(yōu)先級,以便正確的線程在正確的時間獲得正確的優(yōu)先級。如果設(shè)置得太高,你的線程可能會中斷UI線程和RenderThread,導致你的應用程序丟幀。如果設(shè)置得太低,可以使你的異步任務(wù)(如圖像加載)比他們需要的慢。

每當你創(chuàng)建一個線程,你應該打電話 setThreadPriority()。系統(tǒng)的線程調(diào)度器優(yōu)先考慮高優(yōu)先級的線程,平衡這些優(yōu)先級,最終完成所有工作。一般來說,前臺組中的線程占設(shè)備總執(zhí)行時間的95%左右,而后臺組大約占5%。

系統(tǒng)還使用Process該類為每個線程分配自己的優(yōu)先級值 。

默認情況下,系統(tǒng)將線程的優(yōu)先級設(shè)置為與產(chǎn)卵線程相同的優(yōu)先級和組成員資格。但是,您的應用程序可以使用明確調(diào)整線程優(yōu)先級
setThreadPriority()。

你的應用程序應該將線程的優(yōu)先級設(shè)置為THREAD_PRIORITY_BACKGROUND為執(zhí)行不太緊急工作的線程。

你的應用程序可以使用THREAD_PRIORITY_LESS_FAVORABLETHREAD_PRIORITY_MORE_FAVORABLE常量作為增量來設(shè)置相對優(yōu)先級。有關(guān)線程優(yōu)先級的列表,請參閱類中的THREAD_PRIORITY常量Process。

線程的helper類

Fragment提供了相同的Java類和基本類型,以方便線程,比如Thread,RunnableExecutors類。為了幫助減少與為Android開發(fā)線程應用程序相關(guān)的認知負載,框架提供了一系列可以幫助開發(fā)的幫助程序,例如AsyncTaskLoaderAsyncTask。每個輔助類都有一組特定的性能細微差別,這些細微差別使得它們對于特定的線程問題子集是唯一的。對錯誤的情況使用錯誤的類可能會導致性能問題。

AsyncTask類

本AsyncTask類是需要快速從主線程移動工作到工作線程應用程序的簡單,實用的原始。例如,輸入事件可能觸發(fā)需要用加載的位圖更新UI。一個AsyncTask 對象可以卸載位圖加載和解碼到另一個線程; 一旦處理完成,AsyncTask對象可以管理接收主線程上的工作以更新UI。

使用時AsyncTask,要記住一些重要的性能方面。首先,默認情況下,一個應用程序?qū)syncTask 其創(chuàng)建的所有對象推送到單個線程中。因此,它們以串行方式執(zhí)行,并且與主線程一樣,特別長的工作包可以阻塞隊列。因此,我們建議您僅AsyncTask處理短于5ms的工作項目。

AsyncTask對象也是隱式引用問題的最常見的。 AsyncTask對象也存在與明確引用有關(guān)的風險,但這些有時候更容易解決。例如,AsyncTask 為了正確地更新UI對象,可能需要對UI對象的引用,一旦AsyncTask在主線程上執(zhí)行其回調(diào)。在這種情況下,您可以使用一個WeakReference來存儲對所需UI對象的引用,并AsyncTask在主線程上運行時訪問該對象 。要清楚,持有一個WeakReference對象不會使對象線程安全; 在 WeakReference僅提供處理與明確提到和垃圾收集問題的方法。

HandlerThread類

雖然一個AsyncTask 是有用的, 它可能并不總是正確的解決你的線程問題。相反,您可能需要更傳統(tǒng)的方法來在較長時間運行的線程上執(zhí)行一個工作塊,以及手動管理該工作流的能力。

考慮從Camera對象獲取PreviewFrame的常見挑戰(zhàn) 。當你注冊攝像頭預覽幀時,你會在onPreviewFrame()調(diào)中接收到這些回調(diào),該回調(diào)將在調(diào)用它的事件線程上調(diào)用。如果在UI線程上調(diào)用此回調(diào)函數(shù),則處理巨大像素數(shù)組的任務(wù)將干擾渲染和事件處理工作。同樣的問題也適用于AsyncTask連續(xù)執(zhí)行作業(yè),這容易被阻塞。

這是一個處理程序線程適當?shù)那闆r:處理程序線程實際上是一個長時間運行的線程,它從隊列中抓取工作,并對其進行操作。在這個例子中,當你的應用程序把這個Camera.open()命令委托 給處理程序線程的一個工作塊時,相關(guān)的onPreviewFrame()回調(diào)就落在處理程序線程上,而不是UI或AsyncTask 線程上。所以,如果你要在像素上做長時間的工作,這可能是一個更好的解決方案。

當你的應用程序創(chuàng)建一個使用的線程時HandlerThread,不要忘記 根據(jù)它所做的工作類型來設(shè)置線程的 優(yōu)先級。請記住,CPU只能并行處理少量的線程。設(shè)置優(yōu)先級有助于系統(tǒng)知道正確的方式來安排這項工作,當所有其他線程爭取注意。

ThreadPoolExecutor類

有一些類型的工作可以降低到高度并行的分布式任務(wù)。例如,一個這樣的任務(wù)是為一個8兆像素圖像的每個8×8塊計算濾波器。這個工作包的數(shù)量很大,而且不是合適的類別。單線程本質(zhì)將把所有的線程化工作轉(zhuǎn)化為一個線性系統(tǒng)。另一方面,使用這個類會要求程序員手動管理一組線程之間的負載平衡。

ThreadPoolExecutor是一個輔助類,使這個過程更容易。這個類管理著一組線程的創(chuàng)建,設(shè)置它們的優(yōu)先級,并管理這些線程之間的工作分配方式。隨著工作量的增加或減少,這個類會加速或破壞更多的線程以適應工作負載。

這個類也可以幫助你的應用產(chǎn)生最佳的線程數(shù)量。當它構(gòu)造一個ThreadPoolExecutor對象時,應用程序設(shè)置最小和最大線程數(shù)。由于ThreadPoolExecutor增加的工作量 ,班級將考慮初始化的最小和最大線程數(shù),并考慮待處理的工作量。基于這些因素,ThreadPoolExecutor決定在任何給定時間應該有多少線程活著。

你應該創(chuàng)建多少個線程?

盡管從軟件級別來看,你的代碼有能力創(chuàng)建數(shù)百個線程,但這樣做會造成性能問題。你的應用程序與后臺服務(wù),渲染器,音頻引擎,網(wǎng)絡(luò)等共享有限的CPU資源。CPU實際上只能并行處理少量的線程,上面的所有內(nèi)容都會遇到 優(yōu)先級和調(diào)度問題。因此,只需創(chuàng)建與您的工作負載所需的線程數(shù)量就很重要。

實際上,有很多變量對此負責,但是選擇一個值(比如4,對于初學者),并且使用Systrace進行測試與 其他方法一樣可靠。你可以使用反復試驗來發(fā)現(xiàn)可以使用的最少線程數(shù)量,而不會遇到問題。

決定有多少線程的另一個考慮因素是線程不是免費的:它們占用內(nèi)存。每個線程最少需要64k的內(nèi)存。這在設(shè)備上安裝的許多應用程序中快速增加,尤其是在調(diào)用堆棧顯著增長的情況下。

許多系統(tǒng)進程和第三方庫經(jīng)常會啟動自己的線程池。如果你的應用程序可以重復使用現(xiàn)有的線程池,則這種重用可以通過減少內(nèi)存和處理資源的爭用來幫助提高性能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,361評論 25 708
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,711評論 19 139
  • java 接口的意義-百度 規(guī)范、擴展、回調(diào) 抽象類的意義-樂視 為其子類提供一個公共的類型封裝子類中得重復內(nèi)容定...
    交流電1582閱讀 2,390評論 0 11
  • 當某個應用組件啟動且該應用沒有運行其他任何組件時,Android 系統(tǒng)會使用單個執(zhí)行線程為應用啟動新的 Linux...
    小蕓論閱讀 1,817評論 0 12
  • 不愿意收拾,從八月到今天,從作出離開上海的決定到現(xiàn)在,經(jīng)歷了一個季度之長,卻人動,心不愿意動。 今早起來,突...
    彩繪閱讀 265評論 0 0

友情鏈接更多精彩內(nèi)容