Android線程的正確使用姿勢(轉)

線程是程序員進階的一道重要門檻。對于移動開發(fā)者來說,“將耗時的任務放到子線程去執(zhí)行,以保證UI線程的流暢性”是線程編程的第一金科玉律,但這條鐵則往往也是UI線程不怎么流暢的主因。我們在督促自己更多的使用線程的同時,還需要時刻提醒自己怎么避免線程失控。除了了解各類開線程的API之外,更需要理解線程本身到底是個什么樣的存在,并行是否真的高效?系統(tǒng)是怎么樣去調度線程的?開線程的方式那么多,什么樣的姿勢才正確?

多線程編程之所以復雜原因之一在于其并行的特性,人腦的工作方式更符合單線程串行的特點。一個接著一個的處理任務是大腦最舒服的狀態(tài),頻繁的在任務之間切換會產(chǎn)生“頭痛”這類系統(tǒng)異常。人腦的多任務和計算機的多任務性能差異太大導致我們在設計并行的業(yè)務邏輯之時,很容易犯錯。

另一個復雜點在于線程所帶來的副作用,這些副作用包括但不限于:多線程數(shù)據(jù)安全,死鎖,內存消耗,對象的生命周期管理,UI的卡頓等。每一個新開的線程就像扔進湖面的石子,在你忽視的遠處產(chǎn)生漣漪。

把抽象的東西具像化是我們認知世界的主要方式。線程作為操作系統(tǒng)世界的“公民”之一,是如何被調度獲取到CPU和內存資源的,又怎么樣去和其他“公民”互通有無進而實現(xiàn)效益最大化?把這些實體和行為具像到大腦,像操作系統(tǒng)一樣開“上帝視角”,才能正確掌控線程這頭強大的野獸。

進程優(yōu)先級(Process Priority)

線程寄宿在進程當中,線程的生命周期直接被進程所影響,而進程的存活又和其優(yōu)先級直接相關。在處理進程優(yōu)先級的時候,大部分人靠直覺都能知道前臺進程(Foreground

Process)優(yōu)先級要高于后臺進程(Background

Process)。但這種粗糙的劃分無法滿足操作系統(tǒng)高精度調度的需求。無論Android還是iOS,系統(tǒng)對于Foreground,Background進程有進一步的細化。

Foreground Process

Foreground一般意味著用戶雙眼可見,可見卻不一定是active。在Android的世界里,一個Activity處于前臺之時,如果能采集用戶的input事件,就可以判定為active,如果中途彈出一個Dialog,Dialog變成新的active實體,直接面對用戶的操作。被部分遮擋的activity盡管依然可見,但狀態(tài)卻變?yōu)閕nactive。不能正確的區(qū)分visible和active是很多初級程序員會犯的錯誤。

Background Process

后臺進程同樣有更細的劃分。所謂的Background可以理解為不可見(invisible)。對于不可見的任務,Android也有重要性的區(qū)分。重要的后臺任務定義為Service,如果一個進程包含Service(稱為Service

Process),那么在“重要性”上就會被系統(tǒng)區(qū)別對待,其優(yōu)先級自然會高于不包含Service的進程(稱為Background

Process),最后還剩一類空進程(Empty Process)。Empty

Process初看有些費解,一個Process如果什么都不做,還有什么存在的必要。其實Empty

Process并不Empty,還存在不少的內存占用。

在iOS的世界里,Memory被分為Clean Memory和Dirty Memory,Clean

Memory是App啟動被加載到內存之后原始占用的那一部分內存,一般包括初始的stack, heap, text,

data等segment,Dirty Memory是由于用戶操作所改變的那部分內存,也就是App的狀態(tài)值。系統(tǒng)在出現(xiàn)Low Memory

Warning的時候會首先清掉Dirty

Memory,對于用戶來說,操作的進度就全部丟失了,即使再次點擊App圖標,也是一切從頭開始。但由于Clean

Memory沒有被清除,避免了從磁盤重新讀取app數(shù)據(jù)的io損耗,啟動會變快。這也是為什么很多人會感覺手機重啟后,app打開的速度都比較慢。

同理Android世界當中的Empty Process還保存有App相關的Clean Memory,這部分Memory對于提升App的啟動速度大有幫助。顯而易見Empty Process的優(yōu)先級是最低的。

綜上所述,我們可以把Android世界的Process按優(yōu)先級分為如下幾類:

進程的優(yōu)先級從高到低依次分為五類,越往下,在內存緊張的時候越有可能被系統(tǒng)殺掉。簡而言之,越是容易被用戶感知到的進程,其優(yōu)先級必定更高。

線程調度(Thread Scheduling)

Android系統(tǒng)基于精簡過后的linux內核,其線程的調度受時間片輪轉和優(yōu)先級控制等諸多因素影響。不少初學者會認為某個線程分配到的time slice多少是按照其優(yōu)先級與其它線程優(yōu)先級對比所決定的,這并不完全正確。

Linux系統(tǒng)的調度器在分配time slice的時候,采用的CFS(completely fair

scheduler)策略。這種策略不但會參考單個線程的優(yōu)先級,還會追蹤每個線程已經(jīng)獲取到的time

slice數(shù)量,如果高優(yōu)先級的線程已經(jīng)執(zhí)行了很長時間,但低優(yōu)先級的線程一直在等待,后續(xù)系統(tǒng)會保證低優(yōu)先級的線程也能獲取更多的CPU時間。顯然使用這種調度策略的話,優(yōu)先級高的線程并不一定能在爭取time

slice上有絕對的優(yōu)勢,所以Android系統(tǒng)在線程調度上使用了cgroups的概念,cgroups能更好的凸顯某些線程的重要性,使得優(yōu)先級更高的線程明確的獲取到更多的time

slice。

Android將線程分為多個group,其中兩類group尤其重要。一類是default

group,UI線程屬于這一類。另一類是background group,工作線程應該歸屬到這一類。background

group當中所有的線程加起來總共也只能分配到5~10%的time slice,剩下的全部分配給default

group,這樣設計顯然能保證UI線程繪制UI的流暢性。

有不少人吐槽Android系統(tǒng)之所以不如iOS流暢,是因為UI線程的優(yōu)先級和普通工作線程一致導致的。這其實是個誤會,Android的設計者實際上提供了background

group的概念來降低工作線程的CPU資源消耗,只不過與iOS不同的是,Android開發(fā)者需要顯式的將工作線程歸于background

group。

newThread(newRunnable(){@Overridepublicvoidrun(){Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);}}).start();

所以在我們決定新啟一個線程執(zhí)行任務的時候,首先要問自己這個任務在完成時間上是否重要到要和UI線程爭奪CPU資源。如果不是,降低線程優(yōu)先級將其歸于background group,如果是,則需要進一步的profile看這個線程是否造成UI線程的卡頓。

雖說Android系統(tǒng)在任務調度上是以線程為基礎單位,設置單個thread的優(yōu)先級也可以改變其所屬的control

groups,從而影響CPU time

slice的分配。但進程的屬性變化也會影響到線程的調度,當一個App進入后臺的時候,該App所屬的整個進程都將進入background

group,以確保處于foreground,用戶可見的新進程能獲取到盡可能多的CPU資源。用adb可以查看不同進程的當前調度策略。

$ adb shell ps -P

當你的App重新被用戶切換到前臺的時候,進程當中所屬的線程又會回歸的原來的group。在這些用戶頻繁切換的過程當中,thread的優(yōu)先級并不會發(fā)生變化,但系統(tǒng)在time slice的分配上卻在不停的調整。

是否真的需要新線程?

開線程并不是提升App性能,解決UI卡頓的萬金油。每一個新啟的線程會消耗至少64KB的內存,系統(tǒng)在不同的線程之間switch

context也會帶來額外的開銷。如果隨意開啟新線程,隨著業(yè)務的膨脹,很容易在App運行的某個時間點發(fā)現(xiàn)幾十個線程同時在運行。后果是原本想解決UI流暢性,卻反而導致了偶現(xiàn)的不可控的卡頓。

移動端App新啟線程一般都是為了保證UI的流暢性,增加App用戶操作的響應度。但是否需要將任務放入工作線程需要先了解任務的瓶頸在哪,是i/o,gpu還是cpu?UI出現(xiàn)卡頓并不一定是UI線程出現(xiàn)了費時的計算,有可能是其它原因,比如layout層級太深。

盡量重用已有的工作線程(使用線程池)可以避免出現(xiàn)大量同時活躍的線程,比如對HTTP請求設置最大并發(fā)數(shù)?;蛘邔⑷蝿辗湃肽硞€串行的隊列(HandlerThread)按順序執(zhí)行,工作線程任務隊列適合處理大量耗時較短的任務,避免出現(xiàn)單個任務阻塞整個隊列的情況。

用什么姿勢開線程?

new Thread()

這是Android系統(tǒng)里開線程最簡單的方式,也只能應用于最簡單的場景,簡單的好處卻伴隨不少的隱患。

newThread(newRunnable(){@Overridepublicvoidrun(){}}).start();

這種方式僅僅是起動了一個新的線程,沒有任務的概念,不能做狀態(tài)的管理。start之后,run當中的代碼就一定會執(zhí)行到底,無法中途取消。

Runnable作為匿名內部類還持有了外部類的引用,在線程退出之前,該引用會一直存在,阻礙外部類對象被GC回收,在一段時間內造成內存泄漏。

沒有線程切換的接口,要傳遞處理結果到UI線程的話,需要寫額外的線程切換代碼。

如果從UI線程啟動,則該線程優(yōu)先級默認為Default,歸于default cgroup,會平等的和UI線程爭奪CPU資源。這一點尤其需要注意,在對UI性能要求高的場景下要記得

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

雖說處于background group的線程總共只能爭取到5~10%的CPU資源,但這對絕大部分的后臺任務處理都綽綽有余了,1ms和10ms對用戶來說,都是快到無法感知,所以我們一般都偏向于在background group當中執(zhí)行工作線程任務。

AsyncTask

一個典型的AsyncTask實現(xiàn)如下:

publicclassMyAsyncTaskextendsAsyncTask{@OverrideprotectedObjectdoInBackground(Object[]params){returnnull;}@OverrideprotectedvoidonPreExecute(){super.onPreExecute();}@OverrideprotectedvoidonPostExecute(Objecto){super.onPostExecute(o);}}

和使用Thread()不同的是,多了幾處API回調來嚴格規(guī)范工作線程與UI線程之間的交互。我們大部分的業(yè)務場景幾乎都符合這種規(guī)范,比如去磁盤讀取圖片,縮放處理需要在工作線程執(zhí)行,最后繪制到ImageView控件需要切換到UI線程。

AsyncTask的幾處回調都給了我們機會去中斷任務,在任務狀態(tài)的管理上較之Thread()方式更為靈活。值得注意的是AsyncTask的cancel()方法并不會終止任務的執(zhí)行,開發(fā)者需要自己去檢查cancel的狀態(tài)值來決定是否中止任務。

AsyncTask也有隱式的持有外部類對象引用的問題,需要特別注意防止出現(xiàn)意外的內存泄漏。

AsyncTask由于在不同的系統(tǒng)版本上串行與并行的執(zhí)行行為不一致,被不少開發(fā)者所詬病,這確實是硬傷,絕大部分的多線程場景都需要明確任務是串行還是并行。

線程優(yōu)先級為background,對UI線程的執(zhí)行影響極小。

HandlerThread

在需要對多任務做更精細控制,線程切換更頻繁的場景之下,Thread()和AsyncTask都會顯得力不從心。HandlerThread卻能勝任這些需求甚至更多。

HandlerThread將Handler,Thread,Looper,MessageQueue幾個概念相結合。Handler是線程對外的接口,所有新的message或者runnable都通過handler

post到工作線程。Looper在MessageQueue取到新的任務就切換到工作線程去執(zhí)行。不同的post方法可以讓我們對任務做精細的控制,什么時候執(zhí)行,執(zhí)行的順序都可以控制。HandlerThread最大的優(yōu)勢在于引入MessageQueue概念,可以進行多任務隊列管理。

HandlerThread背后只有一個線程,所以任務是串行執(zhí)行的。串行相對于并行來說更安全,各任務之間不會存在多線程安全問題。

HandlerThread所產(chǎn)生的線程會一直存活,Looper會在該線程中持續(xù)的檢查MessageQueue。這一點和Thread(),AsyncTask都不同,thread實例的重用可以避免線程相關的對象的頻繁重建和銷毀。

HandlerThread較之Thread(),AsyncTask需要寫更多的代碼,但在實用性,靈活度,安全性上都有更好的表現(xiàn)。

ThreadPoolExecutor

Thread(),AsyncTask適合處理單個任務的場景,HandlerThread適合串行處理多任務的場景。當需要并行的處理多任務之時,ThreadPoolExecutor是更好的選擇。

publicstaticExecutorTHREAD_POOL_EXECUTOR=newThreadPoolExecutor(CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE,TimeUnit.SECONDS,sPoolWorkQueue,sThreadFactory);

線程池可以避免線程的頻繁創(chuàng)建和銷毀,顯然性能更好,但線程池并發(fā)的特性往往也是疑難雜癥的源頭,是代碼降級和失控的開始。多線程并行導致的bug往往是偶現(xiàn)的,不方便調試,一旦出現(xiàn)就會耗掉大量的開發(fā)精力。

ThreadPool較之HandlerThread在處理多任務上有更高的靈活性,但也帶來了更大的復雜度和不確定性。

IntentService

不得不說Android在API設計上粒度很細,同一樣工作可以通過各種不同的類來完成。IntentService又是另一種開工作線程的方式,從名字就可以看出這個工作線程會帶有service的屬性。和AsyncTask不同,沒有和UI線程的交互,也不像HandlerThread的工作線程會一直存活。IntentService背后其實也有一個HandlerThread來串行的處理Message

Queue,從IntentService的onCreate方法可以看出:

@OverridepublicvoidonCreate(){// TODO: It would be nice to have an option to hold a partial wakelock// during processing, and to have a static startService(Context, Intent)// method that would launch the service & hand off a wakelock.super.onCreate();HandlerThreadthread=newHandlerThread("IntentService["+mName+"]");thread.start();mServiceLooper=thread.getLooper();mServiceHandler=newServiceHandler(mServiceLooper);}

只不過在所有的Message處理完畢之后,工作線程會自動結束。所以可以把IntentService看做是Service和HandlerThread的結合體,適合需要在工作線程處理UI無關任務的場景。

結束語

Android開線程的方式雖然五花八門,但歸根到底最后還是映射到linux下的pthread,業(yè)務的設計還是脫不了和線程相關的基礎概念范疇:線程的執(zhí)行順序,調度策略,生命周期,串行還是并行,同步還是異步等等。摸清楚各類API下線程的行為特點,在設計具體業(yè)務的線程模型的時候自然輕車熟路了,線程模型的設計要有整個app視角的廣度,切忌各業(yè)務模塊各玩各的。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容