欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

如何正確使用Android線程詳解

 更新時(shí)間:2016年08月14日 15:29:58   投稿:daisy  
線程是程序員進(jìn)階的一道重要門檻。除了了解各類開(kāi)線程的API之外,更需要理解線程本身到底是個(gè)什么樣的存在,并行是否真的高效?系統(tǒng)是怎么樣去調(diào)度線程的?開(kāi)線程的方式那么多,什么樣的姿勢(shì)才正確?下面通過(guò)本文來(lái)好好再學(xué)習(xí)下。

前言

對(duì)于移動(dòng)開(kāi)發(fā)者來(lái)說(shuō),“將耗時(shí)的任務(wù)放到子線程去執(zhí)行,以保證UI線程的流暢性”是線程編程的第一金科玉律,但這條鐵則往往也是UI線程不怎么流暢的主因。我們?cè)诙酱僮约焊嗟氖褂镁€程的同時(shí),還需要時(shí)刻提醒自己怎么避免線程失控。

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

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

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

進(jìn)程優(yōu)先級(jí)(Process Priority)

線程寄宿在進(jìn)程當(dāng)中,線程的生命周期直接被進(jìn)程所影響,而進(jìn)程的存活又和其優(yōu)先級(jí)直接相關(guān)。在處理進(jìn)程優(yōu)先級(jí)的時(shí)候,大部分人靠直覺(jué)都能知道前臺(tái)進(jìn)程(Foreground Process)優(yōu)先級(jí)要高于后臺(tái)進(jìn)程(Background Process)。但這種粗糙的劃分無(wú)法滿足操作系統(tǒng)高精度調(diào)度的需求。無(wú)論Android還是iOS,系統(tǒng)對(duì)于Foreground,Background進(jìn)程有進(jìn)一步的細(xì)化。

Foreground Process

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

Background Process

后臺(tái)進(jìn)程同樣有更細(xì)的劃分。所謂的Background可以理解為不可見(jiàn)(invisible)。對(duì)于不可見(jiàn)的任務(wù),Android也有重要性的區(qū)分。重要的后臺(tái)任務(wù)定義為Service,如果一個(gè)進(jìn)程包含Service(稱為Service Process),那么在“重要性”上就會(huì)被系統(tǒng)區(qū)別對(duì)待,其優(yōu)先級(jí)自然會(huì)高于不包含Service的進(jìn)程(稱為Background Process),最后還剩一類空進(jìn)程(Empty Process)。Empty Process初看有些費(fèi)解,一個(gè)Process如果什么都不做,還有什么存在的必要。其實(shí)Empty Process并不Empty,還存在不少的內(nèi)存占用。

在iOS的世界里,Memory被分為Clean MemoryDirty Memory,Clean Memory是App啟動(dòng)被加載到內(nèi)存之后原始占用的那一部分內(nèi)存,一般包括初始的stack, heap, text, datasegment,Dirty Memory是由于用戶操作所改變的那部分內(nèi)存,也就是App的狀態(tài)值。系統(tǒng)在出現(xiàn)Low Memory Warning的時(shí)候會(huì)首先清掉Dirty Memory,對(duì)于用戶來(lái)說(shuō),操作的進(jìn)度就全部丟失了,即使再次點(diǎn)擊App圖標(biāo),也是一切從頭開(kāi)始。但由于Clean Memory沒(méi)有被清除,避免了從磁盤重新讀取app數(shù)據(jù)的io損耗,啟動(dòng)會(huì)變快。這也是為什么很多人會(huì)感覺(jué)手機(jī)重啟后,app打開(kāi)的速度都比較慢。

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

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

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

線程調(diào)度(Thread Scheduling)

Android系統(tǒng)基于精簡(jiǎn)過(guò)后的linux內(nèi)核,其線程的調(diào)度受時(shí)間片輪轉(zhuǎn)和優(yōu)先級(jí)控制等諸多因素影響。不少初學(xué)者會(huì)認(rèn)為某個(gè)線程分配到的time slice多少是按照其優(yōu)先級(jí)與其它線程優(yōu)先級(jí)對(duì)比所決定的,這并不完全正確。

Linux系統(tǒng)的調(diào)度器在分配time slice的時(shí)候,采用的CFS(completely fair scheduler)策略。這種策略不但會(huì)參考單個(gè)線程的優(yōu)先級(jí),還會(huì)追蹤每個(gè)線程已經(jīng)獲取到的time slice數(shù)量,如果高優(yōu)先級(jí)的線程已經(jīng)執(zhí)行了很長(zhǎng)時(shí)間,但低優(yōu)先級(jí)的線程一直在等待,后續(xù)系統(tǒng)會(huì)保證低優(yōu)先級(jí)的線程也能獲取更多的CPU時(shí)間。顯然使用這種調(diào)度策略的話,優(yōu)先級(jí)高的線程并不一定能在爭(zhēng)取time slice上有絕對(duì)的優(yōu)勢(shì),所以Android系統(tǒng)在線程調(diào)度上使用了cgroups的概念,cgroups能更好的凸顯某些線程的重要性,使得優(yōu)先級(jí)更高的線程明確的獲取到更多的time slice。

Android將線程分為多個(gè)group,其中兩類group尤其重要。一類是default group,UI線程屬于這一類。另一類是background group,工作線程應(yīng)該歸屬到這一類。background group當(dāng)中所有的線程加起來(lái)總共也只能分配到5~10%的time slice,剩下的全部分配給default group,這樣設(shè)計(jì)顯然能保證UI線程繪制UI的流暢性。

有不少人吐槽Android系統(tǒng)之所以不如iOS流暢,是因?yàn)閁I線程的優(yōu)先級(jí)和普通工作線程一致導(dǎo)致的。這其實(shí)是個(gè)誤會(huì),Android的設(shè)計(jì)者實(shí)際上提供了background group的概念來(lái)降低工作線程的CPU資源消耗,只不過(guò)與iOS不同的是,Android開(kāi)發(fā)者需要顯式的將工作線程歸于background group。

new Thread(new Runnable() {
   @Override
   public void run() {
     Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
   }
}).start();

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

雖說(shuō)Android系統(tǒng)在任務(wù)調(diào)度上是以線程為基礎(chǔ)單位,設(shè)置單個(gè)thread的優(yōu)先級(jí)也可以改變其所屬的control groups,從而影響CPU time slice的分配。但進(jìn)程的屬性變化也會(huì)影響到線程的調(diào)度,當(dāng)一個(gè)App進(jìn)入后臺(tái)的時(shí)候,該App所屬的整個(gè)進(jìn)程都將進(jìn)入background group,以確保處于foreground,用戶可見(jiàn)的新進(jìn)程能獲取到盡可能多的CPU資源。用adb可以查看不同進(jìn)程的當(dāng)前調(diào)度策略。

$ adb shell ps -P

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

是否真的需要新線程?

開(kāi)線程并不是提升App性能,解決UI卡頓的萬(wàn)金油。每一個(gè)新啟的線程會(huì)消耗至少64KB的內(nèi)存,系統(tǒng)在不同的線程之間switch context也會(huì)帶來(lái)額外的開(kāi)銷。如果隨意開(kāi)啟新線程,隨著業(yè)務(wù)的膨脹,很容易在App運(yùn)行的某個(gè)時(shí)間點(diǎn)發(fā)現(xiàn)幾十個(gè)線程同時(shí)在運(yùn)行。后果是原本想解決UI流暢性,卻反而導(dǎo)致了偶現(xiàn)的不可控的卡頓。

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

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

用什么姿勢(shì)開(kāi)線程?

new Thread()

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

new Thread(new Runnable() {
  @Override
  public void run() {
 
  }
}).start();

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

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

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

如果從UI線程啟動(dòng),則該線程優(yōu)先級(jí)默認(rèn)為Default,歸于default cgroup,會(huì)平等的和UI線程爭(zhēng)奪CPU資源。這一點(diǎn)尤其需要注意,在對(duì)UI性能要求高的場(chǎng)景下要記得

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

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

AsyncTask

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

public class MyAsyncTask extends AsyncTask {
 
   @Override
   protected Object doInBackground(Object[] params) {
      return null;
   }
 
   @Override
   protected void onPreExecute() {
     super.onPreExecute();
   }
 
   @Override
   protected void onPostExecute(Object o) {
     super.onPostExecute(o);
   }
}

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

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

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

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

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

HandlerThread

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

HandlerThread將Handler,Thread,LooperMessageQueue幾個(gè)概念相結(jié)合。Handler是線程對(duì)外的接口,所有新的message或者runnable都通過(guò)handler post到工作線程。LooperMessageQueue取到新的任務(wù)就切換到工作線程去執(zhí)行。不同的post方法可以讓我們對(duì)任務(wù)做精細(xì)的控制,什么時(shí)候執(zhí)行,執(zhí)行的順序都可以控制。HandlerThread最大的優(yōu)勢(shì)在于引入MessageQueue概念,可以進(jìn)行多任務(wù)隊(duì)列管理。

HandlerThread背后只有一個(gè)線程,所以任務(wù)是串行執(zhí)行的。串行相對(duì)于并行來(lái)說(shuō)更安全,各任務(wù)之間不會(huì)存在多線程安全問(wèn)題。

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

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

ThreadPoolExecutor

Thread(),AsyncTask適合處理單個(gè)任務(wù)的場(chǎng)景,HandlerThread適合串行處理多任務(wù)的場(chǎng)景。當(dāng)需要并行的處理多任務(wù)之時(shí),ThreadPoolExecutor是更好的選擇。

public static Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

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

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

IntentService

不得不說(shuō)Android在API設(shè)計(jì)上粒度很細(xì),同一樣工作可以通過(guò)各種不同的類來(lái)完成。IntentService又是另一種開(kāi)工作線程的方式,從名字就可以看出這個(gè)工作線程會(huì)帶有service的屬性。和AsyncTask不同,沒(méi)有和UI線程的交互,也不像HandlerThread的工作線程會(huì)一直存活。IntentService背后其實(shí)也有一個(gè)HandlerThread來(lái)串行的處理Message Queue,從IntentService的onCreate方法可以看出:

@Override
public void onCreate() {
  // 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();
  HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
  thread.start();
 
  mServiceLooper = thread.getLooper();
  mServiceHandler = new ServiceHandler(mServiceLooper);
}

只不過(guò)在所有的Message處理完畢之后,工作線程會(huì)自動(dòng)結(jié)束。所以可以把IntentService看做是ServiceHandlerThread的結(jié)合體,適合需要在工作線程處理UI無(wú)關(guān)任務(wù)的場(chǎng)景。

總結(jié)

Android開(kāi)線程的方式雖然五花八門,但歸根到底最后還是映射到linux下的pthread,業(yè)務(wù)的設(shè)計(jì)還是脫不了和線程相關(guān)的基礎(chǔ)概念范疇:線程的執(zhí)行順序,調(diào)度策略,生命周期,串行還是并行,同步還是異步等等。摸清楚各類API下線程的行為特點(diǎn),在設(shè)計(jì)具體業(yè)務(wù)的線程模型的時(shí)候自然輕車熟路了,線程模型的設(shè)計(jì)要有整個(gè)app視角的廣度,切忌各業(yè)務(wù)模塊各玩各的。以上就是本文的全部?jī)?nèi)容,希望對(duì)大家開(kāi)發(fā)Android能有所幫助,如果有疑問(wèn)歡迎大家留言討論。

相關(guān)文章

最新評(píng)論