Android | 面試必問的 Handler,你確定不看看?

前言

  • 在 Android 中,Handler 是貫穿于整個應用的消息機制,在面試中出現的概率為:100%
  • 在這篇文章里,我將帶你梳理 Handler 的使用攻略 & 設計原理。追求簡單易懂又不失深度,如果能幫上忙,請務必點贊加關注!

延伸文章


目錄

1. 概述

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

1.1 概念模型

問:Handler 怎么進行線程通信,原理是什么?

消息機制其實并不是 Android 系統獨有的設計,在很多系統設計中都可以看到消息機制的身影,例如 IOS 的 runLoop、Web 的 Ajax 和 Spring 的消息隊列等。在所有系統設計的消息機制里,都會有生產者與消費者的概念,如以下模型:

消息機制概念模型

其中消息緩沖區的具體實現可以是棧 & 隊列,因為隊列(特別是優先級隊列)是最常見的,所以很多情況都會直接將消息緩沖區稱為消息隊列

1.2 架構圖

【類圖】

  • Looper 里組合了 MessageQueue 消息隊列,創建 Looper 的同時也創建了 MessageQueue
  • MessageQueue 里組合了待處理的 Message 鏈表
  • Message 持有用于處理消息的 Handler(target)
  • Handler 被創建時需要聚合 Looper 與 MessageQueue,默認使用的是當前線程的 Looper

1.3 消息的享元模式

消息機制里需要頻繁創建消息對象(Message),因此消息對象需要使用享元模式來緩存,以避免重復分配 & 回收內存。具體來說,Message 使用的是有容量限制的、無頭節點的單鏈表的對象池:

Message 的數據結構

2. Handler 核心源碼分析

這一節我們來分析 Handler 的核心源碼:

2.1 啟動消息循環

  • 問:Looper 如何在子線程中創建?(字節、小米)

要在哪個線程啟動消息循環,就需要在該線程執行Looper.prepare() & Looper.loop()。只有調用Looper.loop()之后,消息循環才算真正運轉起來了。具體來說,啟動消息循環的分為兩種情況:主線程消息循環 & 子線程消息循環,前者由 Framework 啟動,而后者需要我們自己啟動:

  • 主線程消息循環

可以看到,在應用啟動時,Framework 已經為主線程開啟了消息循環,后續我們熟悉的startActivity & startService都是通過主線程消息循環來驅動的。

  • 子線程消息循環

在子線程開啟消息循環,我們需要自己調用Looper.prepare() & Looper.loop();。可以直接創建線程,或者使用 HandlerThread,后者主要考慮的多線程中獲取 Looper 的同步問題,見 第 5.1 節

小結一下:創建 Handler 的代碼需要放在Looper.prepare(); & Looper.loop();中間執行,這是因為創建 Handler 對象時需要聚合 Looper 對象(默認使用的是當前線程的 Looper),而只有執行Looper.prepare();之后,才會創建該線程私有的 Looper 對象,否則創建 Handler 會拋異常。

2.2 Looper 線程唯一性

  • 問:說一下 Looper、handler、線程間的關系。例如一個線程可以對應幾個 Looper、幾個Handler?

  • 問:ThreadLocal 的原理,以及在 Looper 是如何應用的?

每個線程只允許調用一次Looper.prepare(),否則會拋異常。這樣設計是因為一個 Looper 對應了一個消息循環,而一個線程進行多個消息循環是沒有意義的(一個線程不可能同時進行兩個死循環)。那么,Handler 是如何保證 Looper 線程唯一的呢?

答:首先,Handler 主要利用了 ThreadLocal 在每個線程單獨存儲副本的特性,保證了一個ThreadLocal<Looper>在不同線程存取的Looper對象相互獨立;其次,ThreadLocal 是 Looper 的一個static final變量,這樣就保證了整個進程中 sThreadLocal對象不可變;第三,Looper.prepare()判斷在一個線程里重復調用,則會拋出異常。

關于 ThreadLocal 的原理分析,在這篇文章里,我們詳細討論:《Java | ThreadLocal 用法解析》,請關注!

2.3 消息發送

  • 問:Handler#post(Runnable) 是如何執行的?(字節、小米)

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

  • 問:多個 Handler 發消息時,消息隊列如何保證線程安全?

  • 問:為什么 MessageQueue 不設置消息上限?

消息發送的 API 非常多,最終它們都會調用到Handler#sendMessageAtTime(Message msg, long uptimeMillis),內部會交給MessageQueue#enqueueMessage(Message msg, long when)處理,梳理如下:

消息發送調用鏈

消息入隊關鍵源碼

小結一下:

  • 每個消息的處理時間(when)不一樣(SystemClock.uptimeMillis() + delayMill)
  • 消息入隊時,根據消息的處理時間(when)插入排序,隊頭的消息就是最需要執行的消息
  • 當消息隊列為空時(無消息時線程會阻塞),消息入隊需要喚醒線程
  • 當消息隊列不為空時(一般不需要喚醒),只有當開啟同步屏障后第一個異步消息需要喚醒(開啟同步屏障時會在隊首插入一個占位消息,此時消息隊列不為空,但是線程可能是阻塞的),關于同步屏障的內容見第 3 節

2.4 消息獲取

  • 問:消息隊列無消息會怎么樣?為什么 block 不會 ANR?

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

  • 問:Looper 死循環為什么不阻塞主線程?

  • 問:Handler內存泄漏的原因?

上一節我們說到,消息入隊后 Looper 所在線程就會被喚醒(如果被阻塞),以繼續消息循環。在消息循環中,Looper.loop()會死循環從 MessageQueue 獲取隊首的消息,因為消息已經按照處理時間(when)排序,所以每次獲取的都是when最小的消息:

【圖】 loop next

至于 Looper 死循環為什么不會 ANR?

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

ANR:
5秒內沒有響應輸入事件,比如按鍵、屏幕觸摸
10秒內沒有處理廣播
本質:消息隊列中其他消息耗時,按鍵或廣播消息沒有及時處理

根本原因不是線程在睡眠,而是消息隊列被其他耗時消息阻塞,導致按鍵或廣播消息沒有及時處理

Handler內存泄漏的原因
MessageQueue持有Message,Message持有activity
delay多久,message就會持有activity多久
方法:靜態內部類、弱引用

取到一個消息時,如果when還不到,則有限等待(nextPollTimeoutMills)nativePoll()
如果消息隊列沒有消息,則無限等待nativePoll(-1,),而消息入隊時,會執行nativeWake()

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

2.5 消息分發

  • 問:Message.callback 與 Handler.callback 哪個優先?

  • 問:Handler.callback 和 handlemessage() 都存在,但 callback 返回 true,handleMessage() 還會執行么?(字節、小米)

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

【圖】

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

可以看到,除了在Handler#handleMessage(...)中處理消息外,Handler 機制還提供了兩個 Callback 來增加消息處理的靈活性。具體來說,若設置了Message.Callback則優先執行,否則判斷Handler.Callback的返回結果,如果返回false,則最后分發到Handler.handleMessage(...)

2.6 終止消息循環

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

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

3. Handler 同步屏障機制

同步屏障(SyncBarrier)是 Handler 用來篩選高低優先級消息的機制,即:當開啟同步屏障時,高優先級的異步消息優先處理。

3.1 開啟同步屏障

3.2 關閉同步屏障

3.3 同步屏障下的消息循環


4. IdleHandler 機制

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

5. Handler 應用場景

4.1 HandlerThread

Handler 都是在 Looper 所在線程創建的,但是有時候我們需要在其他線程中創建 Looper 所在線程的 Handler,就需要考慮同步問題,使用 HandlerThread 可以簡化這種同步處理:

既然涉及多個線程的通信,會有同步的問題,Android為了簡化Handler的創建過程,提供了HandlerThread類

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

【重點 鎖的機制】

4.2 IntentService

處理完 service 自動停止 內存釋放

4.3 Fragment 生命周期管理

attach -> commit
Glide生命周期管理 RequestManagerFragment 雙重檢查(避免連續兩次with()重復創建Fragment,因為commit會發到Handle消息隊列的)

Handler是貫穿于Android的消息管理機制

所有的代碼都是在Handler上運行的(loop()死循環)


參考資料

推薦閱讀

感謝喜歡!你的點贊是對我最大的鼓勵!歡迎關注彭旭銳的簡書!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375