首先我們思考幾個(gè)問(wèn)題,在Android應(yīng)用中為什么要用多線程?為了解決哪些問(wèn)題?或者為了實(shí)現(xiàn)哪些功能?有哪些好處?請(qǐng)先思考一分鐘,再繼續(xù)往下看。
學(xué)習(xí)而不思考就像吃東西而不嚼,要么無(wú)法下咽,要么嘗不出味道,同時(shí)都會(huì)影響消化吸收。控制一下你那脫韁野馬一樣的好奇心吧,先思考再往下看。
1. 為什么要用多線程
這里列出幾個(gè)原因:
a) 提高用戶體驗(yàn)或者避免ANR
在事件處理代碼中需要使用多線程,否則會(huì)出現(xiàn)ANR(Application is not responding),或者因?yàn)轫憫?yīng)較慢導(dǎo)致用戶體驗(yàn)很差。
圖1 ANR對(duì)話框
b) 異步
應(yīng)用中有些情況下并不一定需要同步阻塞去等待返回結(jié)果,可以通過(guò)多線程來(lái)實(shí)現(xiàn)異步,例如:上一點(diǎn)中提到的,你的應(yīng)用中的某個(gè)Activity需要從云端獲取一些圖片,加載圖片比較耗時(shí),這時(shí)需要使用異步加載,加載完成一個(gè)圖片刷新一個(gè),見(jiàn)下面圖2、圖3 。
c) 多任務(wù)
例如多線程下載。
后兩點(diǎn)與Java中的多線程應(yīng)用沒(méi)有太大區(qū)別,不細(xì)說(shuō)。
下面重點(diǎn)說(shuō)明第一點(diǎn),即如何減少事件響應(yīng)的時(shí)間從而提高用戶體驗(yàn),以及如何避免ANR。
2. 為什么通過(guò)多線程可以提高用戶體驗(yàn)、避免ANR
大家還記得我在群里說(shuō)過(guò)的移動(dòng)開(kāi)發(fā)的“三不要”原則嗎?即:不要讓我想、不要讓我等、不要讓我煩。響應(yīng)慢了用戶需要等,等的次數(shù)多了就會(huì)煩,你的應(yīng)用離被卸載不遠(yuǎn)了。
首先我們來(lái)了解一下Android應(yīng)用程序的main線程,它負(fù)責(zé)處理UI的繪制,Android系統(tǒng)為了防止應(yīng)用程序反應(yīng)較慢導(dǎo)致系統(tǒng)無(wú)法正常運(yùn)行做了一個(gè)處理,一種情況是當(dāng)用戶輸入事件在5秒內(nèi)無(wú)法得到響應(yīng),那么系統(tǒng)會(huì)彈出ANR對(duì)話框,由用戶決定繼續(xù)等待還是強(qiáng)制結(jié)束應(yīng)用程序(另一種情況是BroadcastReciever 超過(guò)10秒沒(méi)執(zhí)行完也會(huì)彈出ANR對(duì)話框)。
即使你的程序中某個(gè)事件響應(yīng)不超過(guò)5秒鐘,人眼可以分辨的時(shí)間是0.1秒,小于0.1秒基本感覺(jué)不出來(lái),超過(guò)0.2秒用戶就能感覺(jué)到有點(diǎn)兒卡了,俗稱(chēng)打嗝現(xiàn)象,2秒以上就很慢了,用戶體驗(yàn)會(huì)很差。有同學(xué)說(shuō)我可以用進(jìn)度條啊,但你的程序中不能到處都是進(jìn)度條,否則那個(gè)圈圈會(huì)把用戶轉(zhuǎn)暈的,好像在對(duì)用戶說(shuō),畫(huà)個(gè)圈圈煩死你……
比如某些應(yīng)用,它要顯示很多圖片,還好它是異步的,不過(guò)在圖片加載完成前每個(gè)圖片的位置上都有一個(gè)圈圈,讓人看了很煩。你可以變通一下,圖片加載成功之前顯示一個(gè)默認(rèn)的圖片,加載成功后再刷新一下即可,何必弄那么多進(jìn)度條呢?
圖2 加載圖片完成前顯示默認(rèn)圖片,加載完成后再刷新
圖3 加載圖片完成前顯示默認(rèn)圖片,加載完成后再刷新
圖4 轉(zhuǎn)暈?zāi)?,煩死?/p>
事件處理的原則:所有可能耗時(shí)的操作都放到其他線程去處理。
Android中的Main線程的事件處理不能太耗時(shí),否則后續(xù)的事件無(wú)法在5秒內(nèi)得到響應(yīng),就會(huì)彈出ANR對(duì)話框。那么哪些方法會(huì)在 Main線程執(zhí)行呢?
1) Activity的生命周期方法,例如:onCreate()、onStart()、onResume()等
2) 事件處理方法,例如onClick()、onItemClick()等
通常Android基類(lèi)中以on開(kāi)頭的方法是在Main線程被回調(diào)的。
提高應(yīng)用的響應(yīng)性,可以從這兩方面入手。
一般來(lái)說(shuō),Activity的onCreate()、onStart()、onResume()方法的執(zhí)行時(shí)間決定了你的應(yīng)用首頁(yè)打開(kāi)的時(shí)間,這里要盡量把不必要的操作放到其他線程去處理,如果仍然很耗時(shí),可以使用SplashScreen。使用SplashScreen最好用動(dòng)態(tài)的,這樣用戶知道你的應(yīng)用沒(méi)有死掉。
圖5 動(dòng)態(tài)SplashScreen
當(dāng)用戶與你的應(yīng)用交互時(shí),事件處理方法的執(zhí)行快慢決定了應(yīng)用的響應(yīng)性是否良好,這里分兩種情況:
1) 同步,需要等待返回結(jié)果。例如用戶點(diǎn)擊了注冊(cè)按鈕,需要等待服務(wù)端返回結(jié)果,那么需要有一個(gè)進(jìn)度條來(lái)提示用戶你的程序正在運(yùn)行沒(méi)有死掉。一般與服務(wù)端交互的都要有進(jìn)度條,例如系統(tǒng)自帶的瀏覽器,URL跳轉(zhuǎn)時(shí)會(huì)有進(jìn)度條。
2) 異步,不需要等待返回結(jié)果。例如微博中的收藏功能,點(diǎn)擊完收藏按鈕后是否成功執(zhí)行完成后告訴我就行了,我不想等它,這里最好實(shí)現(xiàn)為異步的。
無(wú)論同步異步,事件處理都可能比較耗時(shí),那么需要放到其他線程中處理,等處理完成后,再通知界面刷新。
這里有一點(diǎn)要注意,不是所有的界面刷新行為都需要放到Main線程處理,例如TextView的setText()方法需要在Main線程中,否則會(huì)拋出CalledFromWrongThreadException,而ProgressBar的setProgress()方法則不需要在Main線程中處理。
當(dāng)然你也可以把所有UI組件相關(guān)行為都放到Main線程中處理,沒(méi)有問(wèn)題??梢詼p輕你的思考負(fù)擔(dān),但你最好了解他們之間的差別,掌握事物之間細(xì)微差別的是專(zhuān)家。把事件處理代碼放到其他線程中處理,如果處理的結(jié)果需要刷新界面,那么需要線程間通訊的方法來(lái)實(shí)現(xiàn)在其他線程中發(fā)消息給Main線程處理。
3. 如何實(shí)現(xiàn)線程間通訊
在Android中有多種方法可以實(shí)現(xiàn)其他線程與Main線程通訊,我們這里介紹常見(jiàn)的兩種。
1) 使用AsyncTask
AsyncTask是Android框架提供的異步處理的輔助類(lèi),它可以實(shí)現(xiàn)耗時(shí)操作在其他線程執(zhí)行,而處理結(jié)果在Main線程執(zhí)行,對(duì)于開(kāi)發(fā)者而言,它屏蔽掉了多線程和后面要講的Handler的概念。你不了解怎么處理線程間通訊也沒(méi)有關(guān)系,AsyncTask體貼的幫你做好了。不過(guò)封裝越好越高級(jí)的API,對(duì)初級(jí)程序員反而越不利,就是你不了解它的原理。當(dāng)你需要面對(duì)更加復(fù)雜的情況,而高級(jí)API無(wú)法完成得很好時(shí),你就杯具了。所以,我們也要掌握功能更強(qiáng)大,更自由的與Main線程通訊的方法:Handler的使用。
AsyncTask的使用方法見(jiàn)示例工程《EX10_02AsyncTask》
2) 使用Handler
這里需要了解Android SDK提供的幾個(gè)線程間通訊的類(lèi)。
2.1 Handler
Handler在android里負(fù)責(zé)發(fā)送和處理消息,通過(guò)它可以實(shí)現(xiàn)其他線程與Main線程之間的消息通訊。
2.2 Looper
Looper負(fù)責(zé)管理線程的消息隊(duì)列和消息循環(huán)
2.3 Message
Message是線程間通訊的消息載體。兩個(gè)碼頭之間運(yùn)輸貨物,Message充當(dāng)集裝箱的功能,里面可以存放任何你想要傳遞的消息。
2.4 MessageQueue
MessageQueue是消息隊(duì)列,先進(jìn)先出,它的作用是保存有待線程處理的消息。
它們四者之間的關(guān)系是,在其他線程中調(diào)用Handler.sendMsg()方法(參數(shù)是Message對(duì)象),將需要Main線程處理的事件添加到Main線程的MessageQueue中,Main線程通過(guò)MainLooper從消息隊(duì)列中取出Handler發(fā)過(guò)來(lái)的這個(gè)消息時(shí),會(huì)回調(diào)Handler的handlerMessage()方法。
除了以上兩種常用方法之外,還有幾種比較簡(jiǎn)單的方法
3) Activity.runOnUiThread(Runnable)
4) View.post(Runnable)
View.postDelayed(Runnable, long)
5) Handler.post
Handler.postDelayed(Runnable, long)
4. 利用線程池提高性能
這里我們建議使用線程池來(lái)管理臨時(shí)的Thread對(duì)象,從而達(dá)到提高應(yīng)用程序性能的目的。
線程池是資源池在線程應(yīng)用中的一個(gè)實(shí)例。了解線程池之前我們首先要了解一下資源池的概念。在JAVA中,創(chuàng)建和銷(xiāo)毀對(duì)象是比較消耗資源的。我們?nèi)绻趹?yīng)用中需要頻繁創(chuàng)建銷(xiāo)毀某個(gè)類(lèi)型的對(duì)象實(shí)例,這樣會(huì)產(chǎn)生很多臨時(shí)對(duì)象,當(dāng)失去引用的臨時(shí)對(duì)象較多時(shí),虛擬機(jī)會(huì)進(jìn)行垃圾回收(GC),CPU在進(jìn)行GC時(shí)會(huì)導(dǎo)致應(yīng)用程序的運(yùn)行得不到相應(yīng),從而導(dǎo)致應(yīng)用的響應(yīng)性降低。
資源池就是用來(lái)解決這個(gè)問(wèn)題,當(dāng)你需要使用對(duì)象時(shí),從資源池來(lái)獲取,資源池負(fù)責(zé)維護(hù)對(duì)象的生命周期。
了解了資源池,就很好理解線程池了,線程池就是存放對(duì)象類(lèi)型都是線程的資源池。
我增加了如何在其他線程中創(chuàng)建Handler的例子作為選學(xué),前面都掌握好了的同學(xué)可以看一下,如果你需要實(shí)現(xiàn)一個(gè)跟Main線程類(lèi)似的消息處理機(jī)制,需要其他線程可以跟你的線程通訊,可以通過(guò)這種方法實(shí)現(xiàn)。
(審核編輯: 智匯小新)
分享