拆輪子:Handler消息機制的原理分析

找工作面試中這個問題被問到的概率至少是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ù)取出來,然后把第二條變成第一條摩疑。

Message類的obtain方法

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的對象

Handler的構(gòu)造方法

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為空的錯誤窃这。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市凌节,隨后出現(xiàn)的幾起案子钦听,更是在濱河造成了極大的恐慌,老刑警劉巖倍奢,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件朴上,死亡現(xiàn)場離奇詭異,居然都是意外死亡卒煞,警方通過查閱死者的電腦和手機痪宰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畔裕,“玉大人衣撬,你說我怎么就攤上這事“缛模” “怎么了具练?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長甜无。 經(jīng)常有香客問我扛点,道長哥遮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任陵究,我火速辦了婚禮眠饮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘铜邮。我一直安慰自己仪召,他們只是感情好,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布松蒜。 她就那樣靜靜地躺著扔茅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牍鞠。 梳的紋絲不亂的頭發(fā)上咖摹,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音难述,去河邊找鬼萤晴。 笑死,一個胖子當著我的面吹牛胁后,可吹牛的內(nèi)容都是我干的店读。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼攀芯,長吁一口氣:“原來是場噩夢啊……” “哼屯断!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起侣诺,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤殖演,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后年鸳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趴久,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年搔确,在試婚紗的時候發(fā)現(xiàn)自己被綠了彼棍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡膳算,死狀恐怖座硕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涕蜂,我是刑警寧澤华匾,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站机隙,受9級特大地震影響蜘拉,放射性物質(zhì)發(fā)生泄漏刊头。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一诸尽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧印颤,春花似錦您机、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至矢否,卻和暖如春仲闽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背僵朗。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工赖欣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人验庙。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓顶吮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親粪薛。 傳聞我的和親對象是個殘疾皇子悴了,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容