找工作面試中這個問題被問到的概率至少是80%捂敌,跟面試官扯一扯Handler消息的原理惩坑,會讓面試加分很多的。最近我也在找工作朝扼,把這個知識點總結(jié)下赃阀,以備面試之需。
為什么使用Handler機制擎颖?
簡而言之:Android中主線程不能執(zhí)行耗時操作榛斯,會導致ANR異常观游,所有請求網(wǎng)絡(luò)、數(shù)據(jù)大批量操作都在子線程中驮俗,但是子線程又不能更新UI懂缕,所以Google為我們分裝了Handler消息機制。
Android 中主線程也叫 UI 線程王凑,那么從名字上我們也知道主線程主要是用來創(chuàng)建搪柑、更新 UI 的,而其他耗時操作荤崇,比如網(wǎng)絡(luò)訪問拌屏,或者文件處理,多媒體處理等都需要在子線程中操作术荤,之所以在子線程中操作是為了保證 UI 的流暢程度倚喂,手機顯示的刷新頻率是 60Hz,也就是一秒鐘刷新 60 次瓣戚,每 16.67 毫秒刷新一次端圈,為了不丟幀,那么主線程處理代碼最好不要超過 16 毫秒子库。當子線程處理完數(shù)據(jù)后舱权,為了防止 UI 處理邏輯的混亂,Android 只允許主線程修改 UI仑嗅,那么這時候就需要 Handler 來充當子線程和主線程之間的橋梁了宴倍。
1. Handler的解釋
Handler是Android官方給我們提供的一套更新UI線程的機制,也是一套消息處理機制仓技,可以通過Handler來處理消息鸵贬,更新UI等。
2. Handler機制中的四個類
說到Handler消息機制脖捻,我們首先要提到的就是這四個主要類:
2.1 Message
Message 是在線程之間傳遞的消息阔逼,它可以在內(nèi)部攜帶少量的信息,用于在不同線程之間交換數(shù)據(jù)地沮。
Message類的obtain方法:
消息隊列順序的維護是使用單鏈表的形式來維護的嗜浮;
把消息池里的第一條數(shù)據(jù)取出來,然后把第二條變成第一條摩疑。
2.2 MessageQueue
MessageQueue主要包括兩個操作:插入和讀取
讀取操作本身會伴隨這刪除操作
插入和讀取對應的方法分別為enqueueMessage和next
它的內(nèi)部實現(xiàn)并不是隊列危融,而是一個單鏈表的數(shù)據(jù)結(jié)構(gòu)來維護消息列表,單鏈表在插入和刪除上比較有優(yōu)勢
2.3 Looper
Looper扮演著消息循環(huán)的角色
Looper除了prepare方法外未荒,還提供了prepareMainLooper方法专挪,這個方法主要是給主線程也就是ActivityThread創(chuàng)建Looper使用的
Looper提供了quit和quitSafely來退出一個Looper
Looper的loop方法是一個死循環(huán),唯一跳出循環(huán)的方式是MessageQueue的next方法返回null(當Looper的quit方法被調(diào)用時,Looper就會調(diào)用MessageQueue的quit或quitSafely方法來通知消息隊列退出寨腔,當消息隊列被標記為退出狀態(tài)時速侈,它的next方法就會返回null)
Looper處理消息的流程
//獲取消息隊列的消息
Message msg = queue.next(); ? ? ?// might block
...
//分發(fā)消息,消息由哪個handler對象創(chuàng)建迫卢,則由它分發(fā)倚搬,并由它的handlerMessage處理
msg.target.dispatchMessage(msg);
這里的dispatch是在Looper中執(zhí)行的,這樣就成功將代碼邏輯切換到了指定的線程中去執(zhí)行
主線程Looper從消息隊列讀取消息乾蛤,當讀完所有消息時每界,進入睡眠,主線程阻塞家卖。子線程往消息隊列發(fā)送消息眨层,并且往管道文件寫數(shù)據(jù),主線程即被喚醒上荡,從管道文件讀取數(shù)據(jù)趴樱,主線程被喚醒只是為了讀取消息,當消息讀取完畢酪捡,再次睡眠
2.4 Handler
Handler創(chuàng)建時會采用當前線程的Looper來構(gòu)建內(nèi)部的消息循環(huán)系統(tǒng)叁征,如果當前沒有Looper則報錯
Handler通過post或send將msg投遞到MessageQueue中
Handler發(fā)送消息,sendMessage的所有重載逛薇,實際最終都調(diào)用了sendMessageAtTime
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
Looper輪詢消息隊列捺疼,并且運行在創(chuàng)建Handler的線程中, 這樣一來Handler中的業(yè)務邏輯就被切換到創(chuàng)建Handler所在的線程中執(zhí)行了
創(chuàng)建Handler對象時永罚,在構(gòu)造方法中會獲取Looper和MessageQueue的對象
2.5 HandlerThread
HandlerThread是Android官方給我們提供好的一套子線程的Handler啤呼,也就是異步處理機制,它是為了避免線程切換導致空指針異常的錯誤呢袱。
2.6 ThreadLocal
ThreadLocal是一個線程內(nèi)部的數(shù)據(jù)存儲類媳友, 通過它可以在指定的線程中存儲數(shù)據(jù),數(shù)據(jù)存儲以后产捞,只有在指定線程中可以獲取到存儲的數(shù)據(jù),對于其他線程來說則無法獲取到數(shù)據(jù)
Andorid源碼中Looper哼御、ActivityThread以及AMS中都用到了ThreadLocal
ThreadLocal的另一個使用場景是復雜邏輯下的對象傳遞:比如監(jiān)聽器的傳遞
原理分析
ThreadLocal內(nèi)部會從各自的線程中取出一個數(shù)組坯临,然后再從數(shù)組中根據(jù)當前ThreadLocal的索引去查找對應的value值
內(nèi)部實現(xiàn)
set方法
1、通過values方法來獲取當前線程中的ThreadLocal數(shù)據(jù)恋昼,沒有l(wèi)ocalValues則初始化
2看靠、然后再將ThreadLocal的值進行存儲
get方法
1、取出當前線程的localValues對象液肌,沒有則返回初始值
2挟炬、再取出它的table數(shù)組并找出ThreadLocal的reference對象在table數(shù)組中的位置
3、然后table數(shù)組的下一個位置所存儲的數(shù)據(jù)就是ThreadLocal的值
從set和get可以看出,它們操作的對象都是當前線程的localValues對象的table數(shù)組谤祖,因此你不同線程中訪問同一個ThreadLocal的set和get方法婿滓,它們對ThreadLocal所做的讀/寫操作僅限于各自線程的內(nèi)部。
2.6 主線程的消息循環(huán)
Android的主線程就是ActivityThread粥喜,主線程的入口是main凸主,在main方法中系統(tǒng)會通過Looper.prepareMainLooper()來創(chuàng)建主線程的Looper和一個MessageQueue,并通過Looper.loop()來開啟主線程的消息循環(huán)额湘。
ActivityThread通過ApplicationThread和AMS進行進程間通信卿吐,AMS以進程間通信的方式完成ActivityThread的請求后回調(diào)ApplicationThread中的Binder方法,然后ApplicationThread會向H發(fā)送消息锋华,H收到消息后會將ApplicationThread中的邏輯切換到ActivityThread中去執(zhí)行嗡官,即切換到主線程中去執(zhí)行。
3.內(nèi)部循環(huán)原理解析
在主線程中 Android 默認已經(jīng)調(diào)用了 Looper.preper()方法毯焕,調(diào)用該方法的目的是在 Looper 中創(chuàng)建MessageQueue 成員變量并把 Looper 對象綁定到當前線程中衍腥。
當調(diào)用 Handler 的 sendMessage(對象)方法的時候就將 Message 對象添加到了 Looper 創(chuàng)建的 MessageQueue 隊列中,同時給 Message 指定了 target 對象芥丧,其實這個 target 對象就是 Handler 對象紧阔。主線程默認執(zhí)行了 Looper.looper()方法,該方法從 Looper 的成員變量MessageQueue 中取出 Message续担,然后調(diào)用 Message 的 target 對象的handleMessage()方法擅耽。這樣就完成了整個消息機制。
4. AsyncTask簡介
AsyncTask 是一個抽象類物遇,所以如果我們想使用它乖仇,就必須要創(chuàng)建一個子類去繼承它。在繼承時我們可以為 AsyncTask 類指定三個泛型參數(shù)询兴,這三個參數(shù)的用途如下乃沙。
1. Params
在執(zhí)行 AsyncTask 時需要傳入的參數(shù),可用于在后臺任務中使用诗舰。
2. Progress
后臺任務執(zhí)行時警儒,如果需要在界面上顯示當前的進度,則使用這里指定的泛型作為進度單位眶根。
3. Result
當任務執(zhí)行完畢后蜀铲,如果需要對結(jié)果進行返回,則使用這里指定的泛型作為返回值
類型属百。
《第一行代碼》
AsyncTask 中的經(jīng)常需要去重寫的方法有以下四個记劝。
1. onPreExecute()
這個方法會在后臺任務開始執(zhí)行之前調(diào)用,用于進行一些界面上的初始化操作族扰,比
如顯示一個進度條對話框等厌丑。
2. doInBackground(Params...)
這個方法中的所有代碼都會在子線程中運行定欧,我們應該在這里去處理所有的耗時任
務。任務一旦完成就可以通過 return 語句來將任務的執(zhí)行結(jié)果返回怒竿,如果 AsyncTask 的
第三個泛型參數(shù)指定的是 Void砍鸠,就可以不返回任務執(zhí)行結(jié)果。注意愧口,在這個方法中是不
可以進行 UI 操作的睦番,如果需要更新 UI元素,比如說反饋當前任務的執(zhí)行進度耍属,可以調(diào)
用 publishProgress(Progress...)方法來完成托嚣。
3. onProgressUpdate(Progress...)
當在后臺任務中調(diào)用了 publishProgress(Progress...)方法后,這個方法就會很快被調(diào)
用厚骗,方法中攜帶的參數(shù)就是在后臺任務中傳遞過來的示启。在這個方法中可以對 UI 進行操
作,利用參數(shù)中的數(shù)值就可以對界面元素進行相應地更新领舰。
4. onPostExecute(Result)
當后臺任務執(zhí)行完畢并通過 return 語句進行返回時夫嗓,這個方法就很快會被調(diào)用。返
回的數(shù)據(jù)會作為參數(shù)傳遞到此方法中冲秽,可以利用返回的數(shù)據(jù)來進行一些 UI 操作舍咖,比如
說提醒任務執(zhí)行的結(jié)果,以及關(guān)閉掉進度條對話框等锉桑。
如果想要啟動這個任務排霉,只需編寫以下代碼即可:
new DownloadTask().execute();
《第一行代碼》
5. 面試問答題
Handler和AsyncTask的區(qū)別
這倆類都是用來實現(xiàn)異步的,其中AsyncTask的集成度較高民轴,使用簡單攻柠,Handler則需要手動寫Runnable或者Thread的代碼;
另外后裸,由于AsyncTask內(nèi)部實現(xiàn)了一個非常簡單的線程池瑰钮,實際上是只適用于輕量級的異步操作的,一般不應該用于網(wǎng)絡(luò)操作和數(shù)據(jù)庫的大量操作微驶。
只能在UI線程里面更新界面嗎浪谴?
答:不一定,之所以子線程不能更新界面因苹,是因為Android在線程的方法里面采用checkThread進行判斷是否是主線程较店,而這個方法是在ViewRootImpl中的,這個類是在onResume里面才生成的容燕,因此,如果這個時候子線程在onCreate方法里面生成更新UI婚度,而且沒有做阻塞蘸秘,就是耗時多的操作官卡,還是可以更新UI的。
Android子線程更新UI的方式有幾種醋虏?
答:一般情況下寻咒,我們都采用Handler的方式進行更新UI,當然颈嚼,代碼層的實現(xiàn)有不同的方法毛秘,比如可以使用Handler的post方法進行更新UI,或者用Handler的sendMessage方法進行更新UI阻课,或者通過View的post方法進行更新叫挟,還有一個是runOnUIThread也是可以進行更新的。但這些本質(zhì)上還是通過Handler進行子線程的更新限煞。
使用Handler的時候一般會遇到什么問題抹恳?
答:比如說子線程更新UI,是因為觸發(fā)了checkThread方法檢查是否在主線程更新UI署驻,還有就是子線程中沒有Looper奋献,這個原因是因為Handler的機制引起的,因為Handler發(fā)送Message的時候旺上,需要將Message放到MessageQueue里面瓶蚂,而這個時候如果沒有Looper的話,就無法循環(huán)輸出MessageQueue了宣吱,這個時候就會報Looper為空的錯誤窃这。