Android面試必問的 Handler 知識點
前言
- 在 Android 中,Handler 是貫穿於整個應用的消息機制,在面試中出現的概率爲:100%
- 在這篇文章裏,我將帶你梳理 Handler 的使用攻略 & 設計原理。追求簡單易懂又不失深度,如果能幫上忙,請務必點贊加關注!
延伸文章
- 關於ThreadLocal,請閱讀:《Java | ThreadLocal 用法解析》
- 關於EventBus,請閱讀: 《Android | 這是一份詳細的 EventBus 使用教程》
目錄
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
-
原因:是handler驅動的機制,所有的事件都需要Handler處理,例如LAUNCH_ACTIVITY等
3. Handler 同步屏障機制
同步屏障(SyncBarrier)是 Handler 用來篩選高低優先級消息的機制,即:當開啓同步屏障時,高優先級的異步消息優先處理。
3.1 開啓同步屏障
3.2 關閉同步屏障
3.3 同步屏障下的消息循環
瞭解更多同步屏障: https://blog.csdn.net/jdsjlzx/article/details/110563162
4. IdleHandler 機制
- 問:IdleHandler 是什麼?怎麼使用,能解決什麼問題?
詳見:https://blog.csdn.net/jdsjlzx/article/details/110532500
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()死循環)