15分鐘徹底掌握Handler

我們一起分析 Android Handler 的源碼攻柠。

Handler 現(xiàn)在幾乎是 Android 面試的必問知識點(diǎn)了立由,大多數(shù) Android 工程師都在項(xiàng)目中使用過 Handler。主要場景是子線程完成耗時(shí)操作的過程中洼裤,通過 Handler 向主線程發(fā)送消息 Message邻辉,用來刷新 UI 界面。這節(jié)課我們來了解 Handler 的發(fā)送消息和處理消息的源碼實(shí)現(xiàn)。

分析源碼的時(shí)候最好是找到一個(gè)合適的切入點(diǎn)值骇,Handler 源碼的一個(gè)切入點(diǎn)就是它的默認(rèn)構(gòu)造器莹菱。

從 new Handler() 開始

511591752260_.png

在無參構(gòu)造器里調(diào)用了重載的構(gòu)造方法并分別傳入 null 和 false。并且在構(gòu)造方法中給兩個(gè)全局變量賦值:mLooper 和 mQueue吱瘩。

這兩者都是通過 Looper 來獲取道伟,具體代碼如下:


521591768081_.png

可以看出,myLooper 通過一個(gè)線程本地變量中的存根使碾,然后 mQueue 是 Looper 中的一個(gè)全局變量蜜徽,類型是 MessageQueue 類型。

接下來的分析重點(diǎn)就是這個(gè) Looper 是什么票摇?以及何時(shí)被初始化拘鞋?

Looper 介紹

不知你有沒有思考過一個(gè)問題,啟動(dòng)一個(gè) Java 程序的入口函數(shù)是 main 方法矢门,但是當(dāng) main 函數(shù)執(zhí)行完畢之后此程序停止運(yùn)行掐禁,也就是進(jìn)程會自動(dòng)終止。但是當(dāng)我們打開一個(gè) Activity 之后颅和,只要我們不按下返回鍵 Activity 會一直顯示在屏幕上傅事,也就是 Activity 所在進(jìn)程會一直處于運(yùn)行狀態(tài)。實(shí)際上 Looper 內(nèi)部維護(hù)一個(gè)無限循環(huán)峡扩,保證 App 進(jìn)程持續(xù)進(jìn)行蹭越。

Looper初始化

Activity 啟動(dòng)過程時(shí),ActivityThread 的 main 方法是一個(gè)新的 App 進(jìn)程的入口教届,其具體實(shí)現(xiàn)如下:

531591768172_.png

解釋說明:

圖中 1 處就是初始化當(dāng)前進(jìn)程的 Looper 對象响鹃;
圖中 2 處調(diào)用 Looper 的 loop 方法開啟無限循環(huán)。
prepareMainLooper 方法如下:


541591768413_.png

這里我沒有省略任何一行代碼案训,因?yàn)榇颂幍拇a很精簡但是每一行又都有意義买置。

圖中 1 處在 prepareMainLooper 中調(diào)用 prepare 方法創(chuàng)建 Looper 對象,仔細(xì)查看發(fā)現(xiàn)其實(shí)就是 new 出一個(gè) Looper强霎。核心之處在于將 new 出的 Looper 設(shè)置到了線程本地變量 sThreadLocal 中忿项。也就是說創(chuàng)建的 Looper 與當(dāng)前線程發(fā)生了綁定。

Looper 的構(gòu)造方法如下:


551591768596_.png

可以看出城舞,在構(gòu)造方法中初始化了消息隊(duì)列 MessageQueue 對象轩触。

prepare 方法執(zhí)行完之后,會在圖中 3 處調(diào)用 myLooper() 方法家夺,從 sThreadLocal 中取出 Looper 對象并賦值給 sMainLooper 變量脱柱。


561591768607_.png

注意:

圖中 2 處在創(chuàng)建 Looper 對象之前,會判斷 sThreaLocal 中是否已經(jīng)綁定過 Looper 對象拉馋,如果是則拋出異常榨为。這行代碼的目的是確保在一個(gè)線程中 Looper.prepare() 方法只能被調(diào)用 1 次惨好。比如以下代碼:


571591768618_.png

執(zhí)行上述代碼程序會秒崩,打印日志如下:


581591768629_.png

注意:

不是說調(diào)用 2 次 prepare 才會拋異常嗎随闺?為什么 MainActivity 中只調(diào)用了 1 遍就導(dǎo)致程序崩潰日川? 這是因?yàn)樵?MainActivity 所在進(jìn)程被創(chuàng)建時(shí),Looper 的 prepare 方法已經(jīng)在 main 方法中調(diào)用了 1 遍板壮。這會直接導(dǎo)致一個(gè)非常重要的結(jié)果:

prepare 方法在一個(gè)線程中只能被調(diào)用 1 次逗鸣;
Looper 的構(gòu)造方法在一個(gè)線程中只能被調(diào)用 1 次合住;
最終導(dǎo)致 MessageQueue 在一個(gè)線程中只會被初始化 1 次绰精。
也就是說 UI 線程中只會存在 1 個(gè) MessageQueue 對象,后續(xù)我們通過 Handler 發(fā)送的消息都會被發(fā)送到這個(gè) MessageQueue 中透葛。

Looper 負(fù)責(zé)做什么事情

用一句話總結(jié) Looper 做的事情就是:不斷從 MessageQueue 中取出 Message笨使,然后處理 Message 中指定的任務(wù)升熊。

在 ActivityThread 的 main 方法中毯侦,除了調(diào)用 Looper.prepareMainLooper 初始化 Looper 對象之外粱快,還調(diào)用了 Looper.loop 方法開啟無限循環(huán)撩嚼,Looper 的主要功能就是在這個(gè)循環(huán)中完成的晒来。


591591768875_.png

很顯然印屁,loop 方法中執(zhí)行了一個(gè)死循環(huán)雨膨,這也是一個(gè) Android App 進(jìn)程能夠持續(xù)運(yùn)行的原因幕垦。

圖中 1 處不斷地調(diào)用 MessageQueue 的 next 方法取出 Message岳遥。如果 message 不為 null 則調(diào)用圖中 2 處進(jìn)行后續(xù)處理奕翔。具體就是從 Message 中取出 target 對象,然后調(diào)用其 dispatchMessage 方法處理 Message 自身浩蓉。那這個(gè) target 是誰呢派继?查看 Message.java 源碼可以看出 target 就是 Handler 對象,如下所示:


601591768883_.png

Handler 的 dispatchMessage 方法如下:


611591768897_.png

可以看出捻艳,在 dispatchMessage 方法中會調(diào)用一個(gè)空方法 handleMessage驾窟,而這個(gè)方法也正是我們創(chuàng)建 Handler 時(shí)需要覆蓋的方法。那么 Handler 是何時(shí)將其設(shè)置為一個(gè) Message 的 target 的呢认轨?

Handler 的 sendMessage 方法

Handler 有幾個(gè)重載的 sendMessage 方法绅络,但是基本都大同小異。我用最普通的 sendMessage 方法來分析嘁字,代碼具體如下:

611591768897_.png

可以看出昨稼,經(jīng)過幾層調(diào)用之后,sendMessage 最終會調(diào)用 enqueueMessage 方法將 Message 插入到消息隊(duì)列 MessageQueue 中拳锚。而這個(gè)消息隊(duì)列就是我們剛才分析的在 ActivityThread 的 main 方法中通過 Looper 創(chuàng)建的 MessageQueue假栓。

Handler 的 enqueueMessage 方法

621591768911_.png

可以看出:

在圖中 1 處 enqueueMessage 方法中,將 Handler 自身設(shè)置為 Message的target 對象霍掺。因此后續(xù) Message 會調(diào)用此 Handler 的 dispatchMessage 來處理匾荆;
圖中 2 處會判斷如果 Message 中的 target 沒有被設(shè)置拌蜘,則直接拋出異常;
圖中 3 處會按照 Message 的時(shí)間 when 來有序得插入 MessageQueue 中牙丽,可以看出 MessageQueue 實(shí)際上是一個(gè)有序隊(duì)列简卧,只不過是按照 Message 的執(zhí)行時(shí)間來排序。
至此 Handler 的發(fā)送消息和消息處理流程已經(jīng)介紹完畢烤芦,接下來看幾個(gè)面試中經(jīng)常被問到的與 Handler 相關(guān)的題目举娩。

Handler 的 post(Runnable) 與 sendMessage 有什么區(qū)別

看一下 post(Runnable) 的源碼實(shí)現(xiàn)如下:


631591769178_.png

實(shí)際上 post(Runnable) 會將 Runnable 賦值到 Message 的 callback 變量中,那么這個(gè) Runnable 是在什么地方被執(zhí)行的呢构罗?Looper 從 MessageQueue 中取出 Message 之后铜涉,會調(diào)用 dispatchMessage 方法進(jìn)行處理,再看下其實(shí)現(xiàn):


641591769188_.png

可以看出遂唧,dispatchMessage 分兩種情況:

如果 Message 的 Callback 不為 null芙代,一般為通過 post(Runnabl) 方式,會直接執(zhí)行 Runnable 的 run 方法盖彭。因此這里的 Runnable 實(shí)際上就是一個(gè)回調(diào)接口纹烹,跟線程 Thread 沒有任何關(guān)系。
如果 Message 的 Callback 為 null召边,這種一般為 sendMessage 的方式铺呵,則會調(diào)用 Handler 的 hanlerMessage 方法進(jìn)行處理。
Looper.loop() 為什么不會阻塞主線程

剛才我們了解了隧熙,Looper 中的 loop 方法實(shí)際上是一個(gè)死循環(huán)片挂。但是我們的 UI 線程卻并沒有被阻塞,反而還能夠進(jìn)行各種手勢操作贱鼻,這是為什么呢宴卖?在 MessageQueue 的 next 方法中,有如下一段代碼:

651591769196_.png

nativePollOnce 方法是一個(gè) native 方法邻悬,當(dāng)調(diào)用此 native 方法時(shí)症昏,主線程會釋放 CPU 資源進(jìn)入休眠狀態(tài),直到下條消息到達(dá)或者有事務(wù)發(fā)生父丰,通過往 pipe 管道寫端寫入數(shù)據(jù)來喚醒主線程工作肝谭,這里采用的 epoll 機(jī)制。關(guān)于 nativePollOnce 的詳細(xì)分析可以參考:nativePollOnce函數(shù)分析蛾扇。

Handler 的 sendMessageDelayed 或者 postDelayed 是如何實(shí)現(xiàn)的

之前我已經(jīng)介紹過攘烛,在向 MessageQueue 隊(duì)列中插入 Message 時(shí),會根據(jù) Message 的執(zhí)行時(shí)間排序镀首。而消息的延時(shí)處理的核心實(shí)現(xiàn)是在獲取 Message 的階段坟漱,接下來看下 MessageQueue 的 next 方法。


661591769209_.png

圖中藍(lán)框處表示從 MessageQueue 中取出一個(gè) Message更哄,但是當(dāng)前的系統(tǒng)時(shí)間小于 Message.when芋齿,因此會計(jì)算一個(gè) timeout腥寇,目的是實(shí)現(xiàn)在 timeout 時(shí)間段后再將 UI 線程喚醒,因此后續(xù)處理 Message 的代碼只會在 timeout 時(shí)間之后才會被 CPU 執(zhí)行觅捆。

注意:在上述代碼中也能看出赦役,如果當(dāng)前系統(tǒng)時(shí)間大于或等于 Message.when,那么會返回 Message 給 Looper.loop()栅炒。但是這個(gè)邏輯只能保證在 when 之前消息不被處理掂摔,不能夠保證一定在 when 時(shí)被處理。

總結(jié)

應(yīng)用啟動(dòng)是從 ActivityThread 的 main 開始的赢赊,先是執(zhí)行了 Looper.prepare()乙漓,該方法先是 new 了一個(gè) Looper 對象,在私有的構(gòu)造方法中又創(chuàng)建了 MessageQueue 作為此 Looper 對象的成員變量域携,Looper 對象通過 ThreadLocal 綁定 MainThread 中簇秒;
當(dāng)我們創(chuàng)建 Handler 子類對象時(shí)鱼喉,在構(gòu)造方法中通過 ThreadLocal 獲取綁定的 Looper 對象秀鞭,并獲取此 Looper 對象的成員變量 MessageQueue 作為該 Handler 對象的成員變量;
在子線程中調(diào)用上一步創(chuàng)建的 Handler 子類對象的 sendMesage(msg) 方法時(shí)扛禽,在該方法中將 msg 的 target 屬性設(shè)置為自己本身锋边,同時(shí)調(diào)用成員變量 MessageQueue 對象的 enqueueMessag() 方法將 msg 放入 MessageQueue 中;
主線程創(chuàng)建好之后编曼,會執(zhí)行 Looper.loop() 方法豆巨,該方法中獲取與線程綁定的 Looper 對象,繼而獲取該 Looper 對象的成員變量 MessageQueue 對象掐场,并開啟一個(gè)會阻塞(不占用資源)的死循環(huán)往扔,只要 MessageQueue 中有 msg,就會獲取該 msg熊户,并執(zhí)行 msg.target.dispatchMessage(msg) 方法(msg.target 即上一步引用的 handler 對象)萍膛,此方法中調(diào)用了我們第二步創(chuàng)建 handler 子類對象時(shí)覆寫的 handleMessage() 方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嚷堡,一起剝皮案震驚了整個(gè)濱河市蝗罗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蝌戒,老刑警劉巖串塑,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異北苟,居然都是意外死亡桩匪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門友鼻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來傻昙,“玉大人瑟慈,你說我怎么就攤上這事∥葚埃” “怎么了葛碧?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長过吻。 經(jīng)常有香客問我进泼,道長,這世上最難降的妖魔是什么纤虽? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任乳绕,我火速辦了婚禮,結(jié)果婚禮上逼纸,老公的妹妹穿的比我還像新娘洋措。我一直安慰自己,他們只是感情好杰刽,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布菠发。 她就那樣靜靜地躺著,像睡著了一般贺嫂。 火紅的嫁衣襯著肌膚如雪滓鸠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天第喳,我揣著相機(jī)與錄音糜俗,去河邊找鬼。 笑死曲饱,一個(gè)胖子當(dāng)著我的面吹牛悠抹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播扩淀,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼楔敌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了引矩?” 一聲冷哼從身側(cè)響起梁丘,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎旺韭,沒想到半個(gè)月后氛谜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡区端,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年值漫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片织盼。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杨何,死狀恐怖酱塔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情危虱,我是刑警寧澤羊娃,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站埃跷,受9級特大地震影響蕊玷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弥雹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一垃帅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧剪勿,春花似錦贸诚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至赴涵,卻和暖如春媒怯,著一層夾襖步出監(jiān)牢的瞬間订讼,已是汗流浹背髓窜。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留欺殿,地道東北人寄纵。 一個(gè)月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像脖苏,于是被迫代替她去往敵國和親程拭。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359