Android | 面試必問的 Handler廓握,你確定不看看搅窿?

前言

  • 在 Android 中炸客,Handler 是貫穿于整個(gè)應(yīng)用的消息機(jī)制,在面試中出現(xiàn)的概率為:100%
  • 在這篇文章里戈钢,我將帶你梳理 Handler 的使用攻略 & 設(shè)計(jì)原理痹仙。追求簡單易懂又不失深度,如果能幫上忙殉了,請務(wù)必點(diǎn)贊加關(guān)注开仰!

延伸文章


目錄

1. 概述

在 Android 中众弓,很多地方是通過消息機(jī)制驅(qū)動的,例如線程間通信隔箍、四大組件的啟動等等谓娃。消息機(jī)制中主要涉及 的類有:Handler & Looper & MessageQueue & Message,其中 Handler 可以說是消息機(jī)制提供給 Java 層的上層接口蜒滩。

1.1 概念模型

問:Handler 怎么進(jìn)行線程通信滨达,原理是什么?

消息機(jī)制其實(shí)并不是 Android 系統(tǒng)獨(dú)有的設(shè)計(jì)俯艰,在很多系統(tǒng)設(shè)計(jì)中都可以看到消息機(jī)制的身影捡遍,例如 IOS 的 runLoop、Web 的 Ajax 和 Spring 的消息隊(duì)列等竹握。在所有系統(tǒng)設(shè)計(jì)的消息機(jī)制里画株,都會有生產(chǎn)者與消費(fèi)者的概念,如以下模型:

消息機(jī)制概念模型

其中消息緩沖區(qū)的具體實(shí)現(xiàn)可以是棧 & 隊(duì)列啦辐,因?yàn)殛?duì)列(特別是優(yōu)先級隊(duì)列)是最常見的谓传,所以很多情況都會直接將消息緩沖區(qū)稱為消息隊(duì)列

1.2 架構(gòu)圖

【類圖】

  • Looper 里組合了 MessageQueue 消息隊(duì)列芹关,創(chuàng)建 Looper 的同時(shí)也創(chuàng)建了 MessageQueue
  • MessageQueue 里組合了待處理的 Message 鏈表
  • Message 持有用于處理消息的 Handler(target)
  • Handler 被創(chuàng)建時(shí)需要聚合 Looper 與 MessageQueue续挟,默認(rèn)使用的是當(dāng)前線程的 Looper

1.3 消息的享元模式

消息機(jī)制里需要頻繁創(chuàng)建消息對象(Message),因此消息對象需要使用享元模式來緩存充边,以避免重復(fù)分配 & 回收內(nèi)存庸推。具體來說,Message 使用的是有容量限制的浇冰、無頭節(jié)點(diǎn)的單鏈表的對象池:

Message 的數(shù)據(jù)結(jié)構(gòu)

2. Handler 核心源碼分析

這一節(jié)我們來分析 Handler 的核心源碼:

2.1 啟動消息循環(huán)

  • 問:Looper 如何在子線程中創(chuàng)建贬媒?(字節(jié)、小米)

要在哪個(gè)線程啟動消息循環(huán)肘习,就需要在該線程執(zhí)行Looper.prepare() & Looper.loop()际乘。只有調(diào)用Looper.loop()之后,消息循環(huán)才算真正運(yùn)轉(zhuǎn)起來了漂佩。具體來說脖含,啟動消息循環(huán)的分為兩種情況:主線程消息循環(huán) & 子線程消息循環(huán)罪塔,前者由 Framework 啟動,而后者需要我們自己啟動:

  • 主線程消息循環(huán)

可以看到养葵,在應(yīng)用啟動時(shí)征堪,F(xiàn)ramework 已經(jīng)為主線程開啟了消息循環(huán),后續(xù)我們熟悉的startActivity & startService都是通過主線程消息循環(huán)來驅(qū)動的关拒。

  • 子線程消息循環(huán)

在子線程開啟消息循環(huán)佃蚜,我們需要自己調(diào)用Looper.prepare() & Looper.loop();∽虐恚可以直接創(chuàng)建線程谐算,或者使用 HandlerThread,后者主要考慮的多線程中獲取 Looper 的同步問題归露,見 第 5.1 節(jié)洲脂。

小結(jié)一下:創(chuàng)建 Handler 的代碼需要放在Looper.prepare(); & Looper.loop();中間執(zhí)行,這是因?yàn)閯?chuàng)建 Handler 對象時(shí)需要聚合 Looper 對象(默認(rèn)使用的是當(dāng)前線程的 Looper)剧包,而只有執(zhí)行Looper.prepare();之后恐锦,才會創(chuàng)建該線程私有的 Looper 對象,否則創(chuàng)建 Handler 會拋異常玄捕。

2.2 Looper 線程唯一性

  • 問:說一下 Looper踩蔚、handler、線程間的關(guān)系枚粘。例如一個(gè)線程可以對應(yīng)幾個(gè) Looper、幾個(gè)Handler飘蚯?

  • 問:ThreadLocal 的原理馍迄,以及在 Looper 是如何應(yīng)用的?

每個(gè)線程只允許調(diào)用一次Looper.prepare()局骤,否則會拋異常攀圈。這樣設(shè)計(jì)是因?yàn)橐粋€(gè) Looper 對應(yīng)了一個(gè)消息循環(huán),而一個(gè)線程進(jìn)行多個(gè)消息循環(huán)是沒有意義的(一個(gè)線程不可能同時(shí)進(jìn)行兩個(gè)死循環(huán))峦甩。那么赘来,Handler 是如何保證 Looper 線程唯一的呢?

答:首先凯傲,Handler 主要利用了 ThreadLocal 在每個(gè)線程單獨(dú)存儲副本的特性犬辰,保證了一個(gè)ThreadLocal<Looper>在不同線程存取的Looper對象相互獨(dú)立;其次冰单,ThreadLocal 是 Looper 的一個(gè)static final變量幌缝,這樣就保證了整個(gè)進(jìn)程中 sThreadLocal對象不可變;第三诫欠,Looper.prepare()判斷在一個(gè)線程里重復(fù)調(diào)用涵卵,則會拋出異常浴栽。

關(guān)于 ThreadLocal 的原理分析,在這篇文章里轿偎,我們詳細(xì)討論:《Java | ThreadLocal 用法解析》典鸡,請關(guān)注!

2.3 消息發(fā)送

  • 問:Handler#post(Runnable) 是如何執(zhí)行的坏晦?(字節(jié)椿每、小米)

  • 問:Handler#sendMessage() 和 Handler#postDelay() 的區(qū)別?(字節(jié))

  • 問:多個(gè) Handler 發(fā)消息時(shí)英遭,消息隊(duì)列如何保證線程安全间护?

  • 問:為什么 MessageQueue 不設(shè)置消息上限?

消息發(fā)送的 API 非常多挖诸,最終它們都會調(diào)用到Handler#sendMessageAtTime(Message msg, long uptimeMillis)汁尺,內(nèi)部會交給MessageQueue#enqueueMessage(Message msg, long when)處理,梳理如下:

消息發(fā)送調(diào)用鏈

消息入隊(duì)關(guān)鍵源碼

小結(jié)一下:

  • 每個(gè)消息的處理時(shí)間(when)不一樣(SystemClock.uptimeMillis() + delayMill)
  • 消息入隊(duì)時(shí)多律,根據(jù)消息的處理時(shí)間(when)插入排序痴突,隊(duì)頭的消息就是最需要執(zhí)行的消息
  • 當(dāng)消息隊(duì)列為空時(shí)(無消息時(shí)線程會阻塞),消息入隊(duì)需要喚醒線程
  • 當(dāng)消息隊(duì)列不為空時(shí)(一般不需要喚醒)狼荞,只有當(dāng)開啟同步屏障后第一個(gè)異步消息需要喚醒(開啟同步屏障時(shí)會在隊(duì)首插入一個(gè)占位消息辽装,此時(shí)消息隊(duì)列不為空,但是線程可能是阻塞的)相味,關(guān)于同步屏障的內(nèi)容見第 3 節(jié)

2.4 消息獲取

  • 問:消息隊(duì)列無消息會怎么樣拾积?為什么 block 不會 ANR?

  • 問:Looper 死循環(huán)為什么不會 ANR?(B站)

  • 問:Looper 死循環(huán)為什么不阻塞主線程丰涉?

  • 問:Handler內(nèi)存泄漏的原因拓巧?

上一節(jié)我們說到,消息入隊(duì)后 Looper 所在線程就會被喚醒(如果被阻塞)一死,以繼續(xù)消息循環(huán)肛度。在消息循環(huán)中,Looper.loop()會死循環(huán)從 MessageQueue 獲取隊(duì)首的消息投慈,因?yàn)橄⒁呀?jīng)按照處理時(shí)間(when)排序承耿,所以每次獲取的都是when最小的消息:

【圖】 loop next

至于 Looper 死循環(huán)為什么不會 ANR?

消息隊(duì)列中無消息怎么處理 block
nativePollOnce值為-1表示無限等待,讓出cpu時(shí)間片給其線程伪煤,本線程等待
0表示無須等待直接返回
nativePollOnce -> epoll(linux) ->linux層的messagequeue
msg -> 5s -> ANRmsg

ANR:
5秒內(nèi)沒有響應(yīng)輸入事件加袋,比如按鍵、屏幕觸摸
10秒內(nèi)沒有處理廣播
本質(zhì):消息隊(duì)列中其他消息耗時(shí)带族,按鍵或廣播消息沒有及時(shí)處理

根本原因不是線程在睡眠锁荔,而是消息隊(duì)列被其他耗時(shí)消息阻塞,導(dǎo)致按鍵或廣播消息沒有及時(shí)處理

Handler內(nèi)存泄漏的原因
MessageQueue持有Message,Message持有activity
delay多久阳堕,message就會持有activity多久
方法:靜態(tài)內(nèi)部類跋理、弱引用

取到一個(gè)消息時(shí),如果when還不到恬总,則有限等待(nextPollTimeoutMills)nativePoll()
如果消息隊(duì)列沒有消息前普,則無限等待nativePoll(-1,),而消息入隊(duì)時(shí)壹堰,會執(zhí)行nativeWake()

quit也會nativeWake拭卿,喚醒Looper所在線程 => messagequeue返回null => Looper退出

2.5 消息分發(fā)

  • 問:Message.callback 與 Handler.callback 哪個(gè)優(yōu)先?

  • 問:Handler.callback 和 handlemessage() 都存在贱纠,但 callback 返回 true峻厚,handleMessage() 還會執(zhí)行么?(字節(jié)谆焊、小米)

獲取需要執(zhí)行的消息之后惠桃,將調(diào)用msg.target.dispatchMessage(msg);處理消息,具體如下:

【圖】

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        // 1. 設(shè)置了Message.Callback(Runnable)
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            // 2. 設(shè)置了 Handler.Callback(Callback )
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        // 3. 未設(shè)置 Handler.Callback 或 返回 false
        handleMessage(msg);
    }
}
public interface Callback {
    public boolean handleMessage(Message msg);
}

可以看到辖试,除了在Handler#handleMessage(...)中處理消息外辜王,Handler 機(jī)制還提供了兩個(gè) Callback 來增加消息處理的靈活性。具體來說罐孝,若設(shè)置了Message.Callback則優(yōu)先執(zhí)行呐馆,否則判斷Handler.Callback的返回結(jié)果,如果返回false莲兢,則最后分發(fā)到Handler.handleMessage(...)

2.6 終止消息循環(huán)

quit:
mQuitting = true
removeAllMessage()
nativeWake() 喚醒汹来,程序從nativePollOnce(-1)開始執(zhí)行

主線程Looper不允許退出 quit() 拋異常 mQuitAllowed = false
ActivityThead#main looper.loop() 之后拋異常 
原因:是handler驅(qū)動的機(jī)制,所有的事件都需要Handler處理怒见,例如LAUNCH_ACTIVITY等

3. Handler 同步屏障機(jī)制

同步屏障(SyncBarrier)是 Handler 用來篩選高低優(yōu)先級消息的機(jī)制俗慈,即:當(dāng)開啟同步屏障時(shí),高優(yōu)先級的異步消息優(yōu)先處理遣耍。

3.1 開啟同步屏障

3.2 關(guān)閉同步屏障

3.3 同步屏障下的消息循環(huán)


4. IdleHandler 機(jī)制

  • 問:IdleHandler 是什么?怎么使用炮车,能解決什么問題舵变?

5. Handler 應(yīng)用場景

4.1 HandlerThread

Handler 都是在 Looper 所在線程創(chuàng)建的,但是有時(shí)候我們需要在其他線程中創(chuàng)建 Looper 所在線程的 Handler瘦穆,就需要考慮同步問題纪隙,使用 HandlerThread 可以簡化這種同步處理:

既然涉及多個(gè)線程的通信,會有同步的問題扛或,Android為了簡化Handler的創(chuàng)建過程绵咱,提供了HandlerThread類

wait - notifyAll - 避免prepare之前調(diào)用getLooper()

【重點(diǎn) 鎖的機(jī)制】

4.2 IntentService

處理完 service 自動停止 內(nèi)存釋放

4.3 Fragment 生命周期管理

attach -> commit
Glide生命周期管理 RequestManagerFragment 雙重檢查(避免連續(xù)兩次with()重復(fù)創(chuàng)建Fragment,因?yàn)閏ommit會發(fā)到Handle消息隊(duì)列的)

Handler是貫穿于Android的消息管理機(jī)制

所有的代碼都是在Handler上運(yùn)行的(loop()死循環(huán))


參考資料

推薦閱讀

感謝喜歡麸锉!你的點(diǎn)贊是對我最大的鼓勵钠绍!歡迎關(guān)注彭旭銳的簡書!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末花沉,一起剝皮案震驚了整個(gè)濱河市柳爽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碱屁,老刑警劉巖磷脯,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異娩脾,居然都是意外死亡赵誓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門晦雨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來架曹,“玉大人,你說我怎么就攤上這事闹瞧“笮郏” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵奥邮,是天一觀的道長万牺。 經(jīng)常有香客問我,道長洽腺,這世上最難降的妖魔是什么脚粟? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮蘸朋,結(jié)果婚禮上核无,老公的妹妹穿的比我還像新娘。我一直安慰自己藕坯,他們只是感情好团南,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著炼彪,像睡著了一般吐根。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辐马,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天拷橘,我揣著相機(jī)與錄音,去河邊找鬼。 笑死冗疮,一個(gè)胖子當(dāng)著我的面吹牛萄唇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赌厅,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼穷绵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了特愿?” 一聲冷哼從身側(cè)響起仲墨,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎揍障,沒想到半個(gè)月后目养,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡毒嫡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年癌蚁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兜畸。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡努释,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咬摇,到底是詐尸還是另有隱情伐蒂,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布肛鹏,位于F島的核電站逸邦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏在扰。R本人自食惡果不足惜缕减,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芒珠。 院中可真熱鬧桥狡,春花似錦、人聲如沸皱卓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽好爬。三九已至,卻和暖如春甥啄,著一層夾襖步出監(jiān)牢的瞬間存炮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留穆桂,地道東北人宫盔。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像享完,于是被迫代替她去往敵國和親灼芭。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355