熟練使用Android上的線程可以幫助你提高應(yīng)用程序的性能。本文討論使用線程的幾個(gè)方面:使用UI或主線程、應(yīng)用程序生命周期與線程優(yōu)先級(jí)之間的關(guān)系册赛、以及平臺(tái)提供的幫助管理線程復(fù)雜性的方法门怪。
主線程
當(dāng)用戶啟動(dòng)你的應(yīng)用程序時(shí),Android會(huì)創(chuàng)建一個(gè)新的Linux進(jìn)程以及一個(gè)執(zhí)行線程罚勾。這個(gè)主線程, 也被稱為UI線程
吭狡,負(fù)責(zé)屏幕上發(fā)生的所有事情尖殃。了解它如何工作可以幫助您設(shè)計(jì)您的應(yīng)用程序使用主線程以獲得最佳性能。
主線程
的設(shè)計(jì)非常簡(jiǎn)單:它唯一的工作就是從線程安全的工作隊(duì)列中取出并執(zhí)行工作塊划煮,直到其應(yīng)用程序終止送丰。該框架從各個(gè)地方生成了這些工作的一部分。這些地方包括與生命周期信息相關(guān)的回調(diào)弛秋,用戶事件(如輸入)或來自其他應(yīng)用程序和進(jìn)程的事件器躏。此外,應(yīng)用程序可以自己明確排隊(duì)蟹略,而無需使用框架登失。
應(yīng)用程序 幾乎執(zhí)行任何代碼塊都與事件回調(diào)有關(guān),例如輸入挖炬,布局膨脹或繪制揽浙。當(dāng)事件觸發(fā)事件時(shí),事件發(fā)生的線程將事件推出自身意敛,并進(jìn)入主線程的消息隊(duì)列馅巷。主線程可以為事件提供服務(wù)。
當(dāng)動(dòng)畫或畫面更新發(fā)生時(shí)空闲,系統(tǒng)每隔16ms
左右嘗試執(zhí)行一個(gè)工作塊(負(fù)責(zé)繪制畫面)令杈,以便以每秒60幀的速度平滑地進(jìn)行渲染。為了達(dá)到這個(gè)目標(biāo)碴倾,UI / View
層次結(jié)構(gòu)必須在主線程上更新逗噩。但是,當(dāng)主線程的消息傳遞隊(duì)列包含的任務(wù)太多或太長(zhǎng)時(shí)跌榔,主線程無法完成足夠快的更新時(shí)异雁,應(yīng)用程序應(yīng)將此工作移至工作線程。如果主線程無法在16ms
內(nèi)完成工作塊僧须,用戶可能會(huì)觀察到拖尾纲刀,或者對(duì)輸入的UI響應(yīng)性不足。如果主線程阻塞大約五秒鐘
担平,則系統(tǒng)顯示應(yīng)用程序不響應(yīng) (ANR
)對(duì)話框示绊,允許用戶直接關(guān)閉應(yīng)用程序锭部。
從主線程中移動(dòng)大量或長(zhǎng)時(shí)間的任務(wù),以避免影響用戶輸入的平滑呈現(xiàn)和快速響應(yīng)面褐,是您在應(yīng)用中采用線程的最大原因拌禾。
線程和UI對(duì)象引用
按照設(shè)計(jì),Android視圖對(duì)象不是線程安全的展哭。預(yù)計(jì)應(yīng)用程序?qū)⒃谥骶€程上創(chuàng)建湃窍,使用和銷毀UI對(duì)象。如果你嘗試修改甚至引用主線程以外的線程中的UI對(duì)象匪傍,則結(jié)果可能是異常您市,將無提示失敗、崩潰以及其他未定義的錯(cuò)誤行為役衡。
引用的問題分為兩個(gè)不同的類別:顯式引用
和隱式引用
茵休。
顯式引用
非主線程上的許多任務(wù)都有更新UI對(duì)象的最終目標(biāo)。但是手蝎,如果其中一個(gè)線程訪問視圖層次結(jié)構(gòu)中的對(duì)象泽篮,則可能導(dǎo)致應(yīng)用程序不穩(wěn)定:如果工作線程在任何其他線程正在引用該對(duì)象的同時(shí)更改該對(duì)象的屬性,則結(jié)果是未定義的柑船。
例如,考慮在工作線程上保存對(duì)UI對(duì)象的直接引用的應(yīng)用程序泼各。工作線程上的對(duì)象可能包含對(duì)a的引用 View; 但在工作完成之前鞍时,View將從視圖層次結(jié)構(gòu)中移除。當(dāng)這兩個(gè)動(dòng)作同時(shí)發(fā)生時(shí)扣蜻,引用將View對(duì)象保存在內(nèi)存中并在其上設(shè)置屬性逆巍。但是,用戶從不會(huì)看到這個(gè)對(duì)象莽使,并且一旦對(duì)象的引用消失锐极,該應(yīng)用程序就會(huì)刪除該對(duì)象。
在另一個(gè)例子中芳肌,View對(duì)象包含對(duì)擁有它們的活動(dòng)的引用灵再。如果這個(gè)活動(dòng)被破壞了,但是仍有一個(gè)直接或間接引用它的線程塊亿笤,那么垃圾收集器將不會(huì)收集這個(gè)活動(dòng)翎迁,直到這個(gè)工作塊完成執(zhí)行。
在發(fā)生線程工作的情況下净薛,如果發(fā)生某個(gè)活動(dòng)生命周期事件(如屏幕旋轉(zhuǎn))汪榔,此情形可能會(huì)導(dǎo)致出現(xiàn)問題。系統(tǒng)將無法執(zhí)行垃圾收集肃拜,直到正在進(jìn)行的工作完成痴腌。因此雌团,Activity在垃圾收集發(fā)生之前,內(nèi)存中可能有兩個(gè)對(duì)象士聪。
通過這樣的場(chǎng)景锦援,我們建議您的應(yīng)用程序不要在線程工作任務(wù)中包含對(duì)UI對(duì)象的顯式引用。避免這種引用可以幫助您避免這些類型的內(nèi)存泄漏戚嗅,同時(shí)避免線程爭(zhēng)用雨涛。
在所有情況下,您的應(yīng)用程序只應(yīng)更新主線程上的UI對(duì)象懦胞。這意味著您應(yīng)該制定一個(gè)協(xié)商策略替久,允許多個(gè)線程將工作交流回主線程,主線程將更新實(shí)際UI對(duì)象的工作作為最高活動(dòng)或片段躏尉。
隱式引用
在下面的代碼片段中可以看到一個(gè)帶有線程對(duì)象的常見代碼設(shè)計(jì)缺陷:
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) {...}
}
}
這段代碼中的缺陷是代碼將線程對(duì)象聲明MyAsyncTask
為一些活動(dòng)的非靜態(tài)內(nèi)部類
蚯根。這個(gè)聲明創(chuàng)建了一個(gè)對(duì)包含Activity實(shí)例的隱式引用
。因此胀糜,該對(duì)象包含對(duì)該活動(dòng)的引用颅拦,直到線程工作完成,從而導(dǎo)致引用活動(dòng)的銷毀延遲教藻。這種延遲反過來又會(huì)增加內(nèi)存距帅。
直接解決這個(gè)問題的方法是將你的重載的類實(shí)例定義為靜態(tài)類
,或者在它們自己的文件中去掉隱式引用括堤。
另一個(gè)解決方案是將AsyncTask對(duì)象聲明為靜態(tài)嵌套類
碌秸。這樣做可以消除隱式引用問題,因?yàn)殪o態(tài)嵌套類不同于內(nèi)部類:內(nèi)部類的實(shí)例需要實(shí)例化外部類的實(shí)例悄窃,并且可以直接訪問它的封閉方法和字段實(shí)例讥电。相比之下,靜態(tài)嵌套類不需要引用包含類的實(shí)例轧抗,因此它不包含對(duì)外部類成員的引用恩敌。
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) {...}
}
}
線程和應(yīng)用程序以及活動(dòng)生命周期
應(yīng)用程序生命周期可以影響線程在您的應(yīng)用程序中的工作方式。你可能需要決定一個(gè)線程應(yīng)該或不應(yīng)該在一個(gè)活動(dòng)被破壞之后持續(xù)下去横媚。你還應(yīng)該了解線程優(yōu)先級(jí)與活動(dòng)是在前臺(tái)還是在后臺(tái)運(yùn)行之間的關(guān)系纠炮。
持續(xù)執(zhí)行線程
線程一直持續(xù)到產(chǎn)生它們的活動(dòng)的生命周期。線程繼續(xù)執(zhí)行分唾,不受干擾抗碰,無論創(chuàng)建或破壞活動(dòng)。在某些情況下绽乔,這種持久性是可取的弧蝇。
考慮一種情況,其中一個(gè)活動(dòng)產(chǎn)生一組線程化的工作塊,然后在工作線程可以執(zhí)行塊之前被銷毀看疗。應(yīng)用程序應(yīng)該怎樣處理正在運(yùn)行的程序段沙峻?
如果塊要更新不再存在的用戶界面,則沒有理由繼續(xù)工作两芳。例如摔寨,如果工作是從數(shù)據(jù)庫加載用戶信息,然后更新視圖怖辆,則不再需要該線程是复。
相比之下,工作包可能有一些不完全與用戶界面相關(guān)的好處竖螃。在這種情況下淑廊,你應(yīng)該堅(jiān)持這個(gè)線程。例如特咆,數(shù)據(jù)包可能正在等待下載映像季惩,將其緩存到磁盤,并更新關(guān)聯(lián)的 View對(duì)象腻格。盡管該對(duì)象不再存在画拾,但是在用戶返回被銷毀的活動(dòng)的情況下,下載和緩存圖像的行為仍可能是有幫助的菜职。
手動(dòng)管理所有線程對(duì)象的生命周期響應(yīng)可能變得非常復(fù)雜青抛。如果你不正確地管理它們,你的應(yīng)用程序可能會(huì)遭受內(nèi)存爭(zhēng)用和性能問題酬核。裝載機(jī) 是解決這個(gè)問題的方法之一脂凶。加載程序有助于異步加載數(shù)據(jù),同時(shí)還可以通過配置更改來保存信息愁茁。
線程優(yōu)先級(jí)
如“ 進(jìn)程和應(yīng)用程序生命周期”
中所述,應(yīng)用程序線程獲得的優(yōu)先級(jí)部分取決于應(yīng)用程序生命周期中的應(yīng)用程序的位置亭病。在創(chuàng)建和管理應(yīng)用程序中的線程時(shí)鹅很,重要的是設(shè)置其優(yōu)先級(jí),以便正確的線程在正確的時(shí)間獲得正確的優(yōu)先級(jí)罪帖。如果設(shè)置得太高促煮,你的線程可能會(huì)中斷UI線程和RenderThread
,導(dǎo)致你的應(yīng)用程序丟幀整袁。如果設(shè)置得太低菠齿,可以使你的異步任務(wù)(如圖像加載)比他們需要的慢。
每當(dāng)你創(chuàng)建一個(gè)線程坐昙,你應(yīng)該打電話 setThreadPriority()
绳匀。系統(tǒng)的線程調(diào)度器優(yōu)先考慮高優(yōu)先級(jí)的線程,平衡這些優(yōu)先級(jí),最終完成所有工作疾棵。一般來說戈钢,前臺(tái)組中的線程占設(shè)備總執(zhí)行時(shí)間的95%左右,而后臺(tái)組大約占5%是尔。
系統(tǒng)還使用Process該類為每個(gè)線程分配自己的優(yōu)先級(jí)值 殉了。
默認(rèn)情況下,系統(tǒng)將線程的優(yōu)先級(jí)設(shè)置為與產(chǎn)卵線程相同的優(yōu)先級(jí)和組成員資格拟枚。但是薪铜,您的應(yīng)用程序可以使用明確調(diào)整線程優(yōu)先級(jí)
setThreadPriority()
。
你的應(yīng)用程序應(yīng)該將線程的優(yōu)先級(jí)設(shè)置為THREAD_PRIORITY_BACKGROUND
為執(zhí)行不太緊急工作的線程恩溅。
你的應(yīng)用程序可以使用THREAD_PRIORITY_LESS_FAVORABLE
和THREAD_PRIORITY_MORE_FAVORABLE
常量作為增量來設(shè)置相對(duì)優(yōu)先級(jí)隔箍。有關(guān)線程優(yōu)先級(jí)的列表,請(qǐng)參閱類中的THREAD_PRIORITY
常量Process暴匠。
線程的helper類
Fragment提供了相同的Java類和基本類型鞍恢,以方便線程,比如Thread
每窖,Runnable
和Executors
類帮掉。為了幫助減少與為Android開發(fā)線程應(yīng)用程序相關(guān)的認(rèn)知負(fù)載,框架提供了一系列可以幫助開發(fā)的幫助程序窒典,例如AsyncTaskLoader
和AsyncTask
蟆炊。每個(gè)輔助類都有一組特定的性能細(xì)微差別,這些細(xì)微差別使得它們對(duì)于特定的線程問題子集是唯一的瀑志。對(duì)錯(cuò)誤的情況使用錯(cuò)誤的類可能會(huì)導(dǎo)致性能問題涩搓。
AsyncTask類
本AsyncTask類是需要快速從主線程移動(dòng)工作到工作線程應(yīng)用程序的簡(jiǎn)單,實(shí)用的原始劈猪。例如昧甘,輸入事件可能觸發(fā)需要用加載的位圖更新UI。一個(gè)AsyncTask 對(duì)象可以卸載位圖加載和解碼到另一個(gè)線程; 一旦處理完成战得,AsyncTask對(duì)象可以管理接收主線程上的工作以更新UI充边。
使用時(shí)AsyncTask,要記住一些重要的性能方面常侦。首先浇冰,默認(rèn)情況下,一個(gè)應(yīng)用程序?qū)syncTask 其創(chuàng)建的所有對(duì)象推送到單個(gè)線程中聋亡。因此肘习,它們以串行方式
執(zhí)行,并且與主線程一樣坡倔,特別長(zhǎng)的工作包可以阻塞隊(duì)列漂佩。因此脖含,我們建議您僅AsyncTask處理短于5ms的工作項(xiàng)目。
AsyncTask對(duì)象也是隱式引用問題的最常見的仅仆。 AsyncTask對(duì)象也存在與明確引用有關(guān)的風(fēng)險(xiǎn)器赞,但這些有時(shí)候更容易解決。例如墓拜,AsyncTask 為了正確地更新UI對(duì)象港柜,可能需要對(duì)UI對(duì)象的引用,一旦AsyncTask在主線程上執(zhí)行其回調(diào)咳榜。在這種情況下夏醉,您可以使用一個(gè)WeakReference
來存儲(chǔ)對(duì)所需UI對(duì)象的引用,并AsyncTask在主線程上運(yùn)行時(shí)訪問該對(duì)象 涌韩。要清楚畔柔,持有一個(gè)WeakReference
對(duì)象不會(huì)使對(duì)象線程安全; 在 WeakReference
僅提供處理與明確提到和垃圾收集問題的方法。
HandlerThread類
雖然一個(gè)AsyncTask 是有用的臣樱, 它可能并不總是正確的解決你的線程問題靶擦。相反,您可能需要更傳統(tǒng)的方法來在較長(zhǎng)時(shí)間運(yùn)行的線程上執(zhí)行一個(gè)工作塊雇毫,以及手動(dòng)管理該工作流的能力玄捕。
考慮從Camera對(duì)象獲取PreviewFrame的常見挑戰(zhàn) 。當(dāng)你注冊(cè)攝像頭預(yù)覽幀時(shí)棚放,你會(huì)在onPreviewFrame()
調(diào)中接收到這些回調(diào)枚粘,該回調(diào)將在調(diào)用它的事件線程上調(diào)用。如果在UI線程上調(diào)用此回調(diào)函數(shù)飘蚯,則處理巨大像素?cái)?shù)組的任務(wù)將干擾渲染和事件處理工作馍迄。同樣的問題也適用于AsyncTask連續(xù)執(zhí)行作業(yè),這容易被阻塞局骤。
這是一個(gè)處理程序線程適當(dāng)?shù)那闆r:處理程序線程實(shí)際上是一個(gè)長(zhǎng)時(shí)間運(yùn)行的線程攀圈,它從隊(duì)列中抓取工作,并對(duì)其進(jìn)行操作峦甩。在這個(gè)例子中量承,當(dāng)你的應(yīng)用程序把這個(gè)Camera.open()
命令委托 給處理程序線程的一個(gè)工作塊時(shí),相關(guān)的onPreviewFrame()
回調(diào)就落在處理程序線程上穴店,而不是UI或AsyncTask 線程上。所以拿穴,如果你要在像素上做長(zhǎng)時(shí)間的工作泣洞,這可能是一個(gè)更好的解決方案。
當(dāng)你的應(yīng)用程序創(chuàng)建一個(gè)使用的線程時(shí)HandlerThread默色,不要忘記 根據(jù)它所做的工作類型來設(shè)置線程的 優(yōu)先級(jí)球凰。請(qǐng)記住,CPU只能并行處理少量的線程。設(shè)置優(yōu)先級(jí)有助于系統(tǒng)知道正確的方式來安排這項(xiàng)工作呕诉,當(dāng)所有其他線程爭(zhēng)取注意缘厢。
ThreadPoolExecutor類
有一些類型的工作可以降低到高度并行的分布式任務(wù)。例如甩挫,一個(gè)這樣的任務(wù)是為一個(gè)8兆像素圖像的每個(gè)8×8塊計(jì)算濾波器贴硫。這個(gè)工作包的數(shù)量很大,而且不是合適的類別伊者。單線程本質(zhì)將把所有的線程化工作轉(zhuǎn)化為一個(gè)線性系統(tǒng)英遭。另一方面,使用這個(gè)類會(huì)要求程序員手動(dòng)管理一組線程之間的負(fù)載平衡亦渗。
ThreadPoolExecutor
是一個(gè)輔助類挖诸,使這個(gè)過程更容易。這個(gè)類管理著一組線程的創(chuàng)建法精,設(shè)置它們的優(yōu)先級(jí)多律,并管理這些線程之間的工作分配方式。隨著工作量的增加或減少搂蜓,這個(gè)類會(huì)加速或破壞更多的線程以適應(yīng)工作負(fù)載狼荞。
這個(gè)類也可以幫助你的應(yīng)用產(chǎn)生最佳的線程數(shù)量。當(dāng)它構(gòu)造一個(gè)ThreadPoolExecutor
對(duì)象時(shí)洛勉,應(yīng)用程序設(shè)置最小和最大線程數(shù)粘秆。由于ThreadPoolExecutor
增加的工作量 ,班級(jí)將考慮初始化的最小和最大線程數(shù)收毫,并考慮待處理的工作量攻走。基于這些因素此再,ThreadPoolExecutor
決定在任何給定時(shí)間應(yīng)該有多少線程活著昔搂。
你應(yīng)該創(chuàng)建多少個(gè)線程?
盡管從軟件級(jí)別來看,你的代碼有能力創(chuàng)建數(shù)百個(gè)線程输拇,但這樣做會(huì)造成性能問題摘符。你的應(yīng)用程序與后臺(tái)服務(wù),渲染器策吠,音頻引擎逛裤,網(wǎng)絡(luò)等共享有限的CPU資源。CPU實(shí)際上只能并行處理少量的線程猴抹,上面的所有內(nèi)容都會(huì)遇到 優(yōu)先級(jí)和調(diào)度問題带族。因此,只需創(chuàng)建與您的工作負(fù)載所需的線程數(shù)量就很重要蟀给。
實(shí)際上蝙砌,有很多變量對(duì)此負(fù)責(zé)阳堕,但是選擇一個(gè)值(比如4,對(duì)于初學(xué)者)择克,并且使用Systrace進(jìn)行測(cè)試與 其他方法一樣可靠恬总。你可以使用反復(fù)試驗(yàn)來發(fā)現(xiàn)可以使用的最少線程數(shù)量,而不會(huì)遇到問題肚邢。
決定有多少線程的另一個(gè)考慮因素是線程不是免費(fèi)的:它們占用內(nèi)存壹堰。每個(gè)線程最少需要64k的內(nèi)存。這在設(shè)備上安裝的許多應(yīng)用程序中快速增加道偷,尤其是在調(diào)用堆棧顯著增長(zhǎng)的情況下缀旁。
許多系統(tǒng)進(jìn)程和第三方庫經(jīng)常會(huì)啟動(dòng)自己的線程池。如果你的應(yīng)用程序可以重復(fù)使用現(xiàn)有的線程池勺鸦,則這種重用可以通過減少內(nèi)存和處理資源的爭(zhēng)用來幫助提高性能并巍。