前言
大家好,我是方木,一名在帝都干飯的程序員。本篇文章已經(jīng)收錄到【面試官爸爸】系列,歡迎各位大兄弟捧場(chǎng)
歡迎關(guān)注我的微信公眾號(hào) 「方木 Rudy」,里面不僅有技術(shù)干貨,也記錄了一位北漂程序員掙扎向上的點(diǎn)點(diǎn)滴滴~
熟悉的黑影
一團(tuán)黑影緩緩向我逼近
他走路不緊不慢,每一步都堅(jiān)定而沉穩(wěn),像沉重的鼓槌一下一下敲在我的心上。稀疏的頭頂上閃耀著高 P 的光芒,銳利的眼神仿佛一下看穿了我的心虛
少頃,他坐在我對(duì)面
“來(lái)面試的?”
”對(duì)...對(duì)對(duì)對(duì)“
”那好,Handler 這東西你會(huì)吧?來(lái)給我講講吧“
什么是 Handler?
Handler 是 Android 的一種消息處理機(jī)制,與 Looper,MessageQueue 綁定,可以用來(lái)進(jìn)行線程的切換。常用于接收子線程發(fā)送的數(shù)據(jù)并在主線程中更新 UI
你剛說(shuō) Handler 可以切換線程,它是怎么實(shí)現(xiàn)的?
“切換線程”其實(shí)是“線程通信”的一種。為了保證主線程不被阻塞,我們常常需要在子線程執(zhí)行一些耗時(shí)任務(wù),執(zhí)行完畢后通知主線程作出相應(yīng)的反應(yīng),這個(gè)過(guò)程就是線程間通信。
Linux 有一種進(jìn)程間通信的方式叫消息隊(duì)列,簡(jiǎn)單來(lái)說(shuō)當(dāng)兩個(gè)進(jìn)程想要通信時(shí),一個(gè)進(jìn)程將消息放入隊(duì)列中,另一個(gè)進(jìn)程從這個(gè)隊(duì)列中讀取消息,從而實(shí)現(xiàn)兩個(gè)進(jìn)程的通信。
Handler 就是基于這一設(shè)計(jì)而實(shí)現(xiàn)的。在 Android 的多線程中,每個(gè)線程都有一個(gè)自己的消息隊(duì)列,線程可以開啟一個(gè)死循環(huán)不斷地從隊(duì)列中讀取消息。
當(dāng) B 線程要和 A 線程通信時(shí),只需要往 A 的消息隊(duì)列中發(fā)送消息,A 的事件循環(huán)就會(huì)讀取這一消息從而實(shí)現(xiàn)線程間通信
呦呵,不錯(cuò)嘛~ 你剛提到了事件循環(huán)和消息隊(duì)列,他們是怎么實(shí)現(xiàn)的呢?
Android 的事件循環(huán)和消息隊(duì)列是通過(guò) Looper 類來(lái)實(shí)現(xiàn)的
Looper.prepare() 是一個(gè)靜態(tài)方法。它會(huì)構(gòu)建出一個(gè) Looper,同時(shí)創(chuàng)建一個(gè) MessageQueue 作為 Looper 的成員變量。MessageQueue 是存放消息的隊(duì)列
當(dāng)調(diào)用 Looper.loop() 方法時(shí),會(huì)在線程內(nèi)部開啟一個(gè)死循環(huán),不斷地從 MessageQueue 中讀取消息,這就是事件循環(huán)
每個(gè) Handler 都與一個(gè) Looper 綁定,Looper 包含 MessageQueue
那這個(gè) Looper 被存放在哪里呢?
Looper 是存放在線程中的。但如何把 Looper 存放在線程中就引入了 Android 消息機(jī)制的另一個(gè)重點(diǎn) --- ThreadLocal
前面我們提到。Looper.prepare() 方法會(huì)創(chuàng)建出一個(gè) Looper,它其實(shí)還做了一件事,就是將 Looper 放入線程的局部變量 ThreadLocal 中。
// Looper.java
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// sThreadLocal是一個(gè)靜態(tài)對(duì)象,類型是ThreadLocal<Looper>
sThreadLocal.set(new Looper(quitAllowed));
}
那么問(wèn)題來(lái)了,什么是 ThreadLocal 呢?
ThreadLocal 又稱線程的局部變量。它最大的神奇之處在于,一個(gè) ThreadLocal 實(shí)例在不同的線程中調(diào)用 get 方法可以取出不同的值。 用一個(gè)例子來(lái)表示這種用法:
fun main() {
val threadLocal = ThreadLocal<Int>()
threadLocal.set(100)
Thread {
threadLocal.set(20)
println("子線程1 ${threadLocal.get()}")
}.start()
Thread {
println("子線程2 ${threadLocal.get()}")
}.start()
println("主線程: ${threadLocal.get()}")
}
// 運(yùn)行結(jié)果:
子線程1 20
主線程: 100
子線程2 null
ThreadLocal 的核心是 set 方法,它的作用總結(jié)成一句話就是:
ThreadLocal.set 可以將一個(gè)實(shí)例變成線程的成員變量
看一下源碼
// ThreadLocal.java
public void set(T value) {
// ① 獲取當(dāng)前線程對(duì)象
Thread t = Thread.currentThread();
// ② 獲取線程的成員屬性map
ThreadLocalMap map = getMap(t);
// ③ 將value放入map中,如果map為空則創(chuàng)建map
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
方法很簡(jiǎn)單,就是根據(jù)當(dāng)前線程獲取線程的一個(gè) map 對(duì)象,然后把 value 放入 map 中,達(dá)到將 value 變成線程的成員變量的目的
多個(gè) Theadlocal 將多個(gè)變量變成線程的成員變量。于是線程就用 ThreadlLocalMap 來(lái)管理,key 就是 threadLocal
知道了它 set 方法的奧秘,get 方法也就很簡(jiǎn)單啦
//ThreadLocal.java
public T get() {
// ① 獲取當(dāng)前線程對(duì)象
Thread t = Thread.currentThread();
// ② 獲取線程對(duì)象的Map
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
// ③ 獲取之前存放的value
return result;
}
}
return setInitialValue();
}
和 set 方法差不多,區(qū)別就是一個(gè)將 value 寫入 map,一個(gè)從 map 中讀取 value。哼哼
不錯(cuò)不錯(cuò),ThreadLocal 就是這么簡(jiǎn)單直接。那你說(shuō)說(shuō)為什么要將 ThreadLocal 作為 Looper 的設(shè)置和獲取工具呢?
因?yàn)?Looper 要放在線程中的,每個(gè)線程只需要一個(gè)事件循環(huán),只需要一個(gè) Looper。事件循環(huán)是個(gè)死循環(huán),多余的事件循環(huán)毫無(wú)意義。ThreadLocal.set 可以將 Looper 設(shè)置為線程的成員變量
同時(shí)為了方便在不同線程中獲取到 Looper,Android 提供了一個(gè)靜態(tài)對(duì)象 Looper.sThreadLocal。這樣在線程內(nèi)部調(diào)用 sThreadLocal.get 就可以獲取線程對(duì)應(yīng)的 Looper 對(duì)象
綜上所述,使用 ThreadLocal 作為 Looper 的設(shè)置和獲取工具是十分方便合理的
好,你剛說(shuō) Looper 是個(gè)死循環(huán)是吧,如果消息隊(duì)列中沒有消息了,這個(gè)死循環(huán)會(huì)一直“空轉(zhuǎn)”嗎?
當(dāng)然不會(huì)!如果事件循環(huán)中沒有消息要處理但仍然執(zhí)行循環(huán),相當(dāng)于無(wú)意義的浪費(fèi) CPU 資源!Android 是不允許這樣的
為了解決這個(gè)問(wèn)題,在 MessageQueue 中,有兩個(gè) native 方法,nativePollOnce 和 nativeWake。
nativePollOnce 表示進(jìn)行一次輪詢,來(lái)查找是否有可以處理的消息,如果沒有就阻塞線程,讓出 CPU 資源
nativeWake 表示喚醒線程
所以這兩個(gè)方法的調(diào)用時(shí)機(jī)也就顯而易見了
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
···
if (needWake) {
nativeWake(mPtr);
}
···
}
在 MessageQueue 類中,enqueueMessage 方法用來(lái)將消息入隊(duì),如果此時(shí)線程是阻塞的,調(diào)用 nativeWake 喚醒線程
// MessageQueue.java
Message next() {
···
nativePollOnce(ptr, nextPollTimeoutMillis);
···
}
next() 方法用來(lái)取出消息。取之前調(diào)用 nativePollOnce() 查詢是否有可以處理的消息,如果沒有則阻塞線程。等待消息入隊(duì)時(shí)喚醒。
不錯(cuò),看來(lái)你對(duì) Looper 循環(huán)的一些邊界處理也注意到了。既然 Looper 是個(gè)死循環(huán),為什么不會(huì)導(dǎo)致 ANR 呢?
首先要明確一下概念。ANR 是應(yīng)用在特定時(shí)間內(nèi)無(wú)法響應(yīng)一個(gè)事件時(shí)拋出的異常。
典型例子的是在主線程中執(zhí)行耗時(shí)任務(wù)。當(dāng)一個(gè)觸摸事件來(lái)臨時(shí),主線程忙于處理耗時(shí)任務(wù)而無(wú)法在 5s 內(nèi)響應(yīng)觸摸事件,此時(shí)就會(huì)拋出 ANR。
但 Looper 死循環(huán)是事件循環(huán)的基石,本身就是 Android 用來(lái)處理一個(gè)個(gè)事件的。正常情況下,觸摸事件會(huì)加入到這個(gè)循環(huán)中被處理。但如果前一個(gè)事件太過(guò)耗時(shí),下一個(gè)事件等待時(shí)間太長(zhǎng)超出特定時(shí)間,這時(shí)才會(huì)產(chǎn)生 ANR。所以 Looper 死循環(huán)并不是產(chǎn)生 ANR 的原因。
好的,看來(lái)這個(gè)小陷阱沒能誤導(dǎo)你。那你說(shuō)說(shuō)消息隊(duì)列中的消息是如何進(jìn)行排序的呢?
這個(gè)就要看 MessageQueue 的 enqueueMessage 方法了
enqueueMessage 是消息的入隊(duì)方法。Handler 在進(jìn)行線程間通信時(shí),會(huì)調(diào)用 sendMessage 將消息發(fā)送到接收消息的線程的消息隊(duì)列中,消息隊(duì)列調(diào)用 enqueueMessage 將消息入隊(duì)。
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
// ① when是消息入隊(duì)的時(shí)間
msg.when = when;
// ② mMessages是鏈表的頭指針,p是哨兵指針
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
// ③ 遍歷鏈表,比較when找到插入位置
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
// ④ 將msg插入到鏈表中
msg.next = p;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
消息入隊(duì)分為 3 步:
① 將入隊(duì)的時(shí)間綁定在 when 屬性上
② 遍歷鏈表,通過(guò)比較 when 找到插入位置
③ 將 msg 插入到鏈表中
這就是消息的排序方式
好的,假如我有一個(gè)消息,想讓它優(yōu)先執(zhí)行,如何提高它的優(yōu)先級(jí)呢?
根據(jù)上個(gè)問(wèn)題,最容易想到的是修改 Message 的 when 屬性。這確實(shí)不失為一種方法,但 Android 為我們提供了更科學(xué)簡(jiǎn)單的方式,異步消息和同步屏障。
在 Android 的消息機(jī)制中,消息分為同步消息、異步消息和同步屏障三種。(沒錯(cuò),同步屏障是 target 屬性為 null 的特殊消息)。通常我們調(diào)用 sendMessage 方法發(fā)送的是同步消息。異步消息需要和同步屏障配合使用,來(lái)提升消息的優(yōu)先級(jí)。
同步屏障理解起來(lái)其實(shí)很簡(jiǎn)單。剛才說(shuō)同步屏障是一種特殊的消息,當(dāng)事件循環(huán)檢測(cè)到同步屏障時(shí),之后的行為不再像之前那樣根據(jù) when 的值一個(gè)個(gè)取消息,而是遍歷整個(gè)消息隊(duì)列,查找到異步消息取出并執(zhí)行。
這個(gè)特殊的消息在消息隊(duì)列中像一個(gè)標(biāo)志,事件循環(huán)探測(cè)到它時(shí)就改變?cè)瓉?lái)的行為,轉(zhuǎn)而去查找異步消息。表現(xiàn)上看起來(lái)像一個(gè)屏障一樣攔住了同步消息。所以形象地稱為同步屏障。
源碼實(shí)現(xiàn)非常非常簡(jiǎn)單:
//MessageQueue.java
Message next() {
···
// ① target為null表明是同步屏障
if (msg != null && msg.target == null) {
// ② 取出異步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
···
}
了解的挺透徹的嘛。那假如說(shuō)我插入了一個(gè)同步屏障,不移除,會(huì)發(fā)生什么事呢?
同步屏障是用來(lái)“攔住”同步消息,處理異步消息的。如果同步屏障不移除,消息隊(duì)列里的異步消息會(huì)一個(gè)一個(gè)被取出處理,知道異步消息被取完。如果此時(shí)隊(duì)列中沒有異步消息了,則線程會(huì)阻塞,隊(duì)列中的同步消息永遠(yuǎn)不會(huì)執(zhí)行。所以同步屏障要及時(shí)移除。
那你知道同步屏障有哪些應(yīng)用場(chǎng)景嗎?
同步屏障的核心作用是提高消息優(yōu)先級(jí),保證 Message 被優(yōu)先處理。Android 為了避免卡頓,應(yīng)用在了 view 繪制中。具體可以看之前關(guān)于 view 繪制的總結(jié)~
為什么使用 Handler 會(huì)有內(nèi)存泄漏問(wèn)題呢?該如何解決呢?
內(nèi)存泄漏歸根到底其實(shí)是生命周期“錯(cuò)位”導(dǎo)致的:一個(gè)對(duì)象本來(lái)應(yīng)該在一個(gè)短的生命周期中被回收,結(jié)果被一個(gè)長(zhǎng)生命周期的對(duì)象引用,導(dǎo)致無(wú)法回收。 Handler 的內(nèi)存泄漏其實(shí)是內(nèi)部類持有外部類引用導(dǎo)致的。
形成方式有兩種:
(1)匿名內(nèi)部類持有外部類引用
class Activity {
var a = 10
fun postRunnable() {
Handler(Looper.getMainLooper()).post(object : Runnable {
override fun run() {
this@Activity.a = 20
}
})
}
}
Handler 在發(fā)送消息時(shí),message.target 屬性就是 handler 本身。message 被發(fā)送到消息隊(duì)列中,被線程持有,線程是一個(gè)無(wú)比“長(zhǎng)”生命周期的對(duì)象,導(dǎo)致 activity 無(wú)法被及時(shí)回收從而引起內(nèi)存泄漏。
解決辦法是在 activity destory 時(shí)及時(shí)移除 runnable
(2)非靜態(tài)內(nèi)部類持有外部類引用
//非靜態(tài)內(nèi)部類
protected class AppHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
}
}
}
解決方案是用靜態(tài)內(nèi)部類,并將外部引用改為弱引用
private static class AppHandler extends Handler {
//弱引用,在垃圾回收時(shí),被回收
WeakReference<Activity> activity;
AppHandler(Activity activity){
this.activity = new WeakReference<Activity>(activity);
}
public void handleMessage(Message message){
switch (message.what){
}
}
}
好的,Handler,Looper 和 MessageQueue 的基礎(chǔ)知識(shí)我基本問(wèn)完了,最后一個(gè)問(wèn)題,你知道 HandlerThread 和 IdleHandler 嗎?它們是用來(lái)干什么的?
HandlerThread 顧名思義就是 Handler+Thread 的結(jié)合體,它本質(zhì)上是一個(gè) Thread。
我們知道,子線程是需要我們通過(guò) Looper.prepare()和 Looper.loop()手動(dòng)開啟事件循環(huán)的。HandlerThread 其實(shí)就幫我們做了這件事,它是一個(gè)實(shí)現(xiàn)了事件循環(huán)的線程。我們可以在這個(gè)線程中做一些 IO 耗時(shí)操作。
IdleHandler 雖然叫 Handler,其實(shí)和同步屏障一樣是一種特殊的”消息"。不同于 Message,它是一個(gè)接口
public static interface IdleHandler{
boolean queueIdle();
}
Idle 是空閑的意思。與同步屏障不同,同步屏障是提高異步消息的優(yōu)先級(jí)使其優(yōu)先執(zhí)行,IdleHandler 是事件循環(huán)出現(xiàn)空閑的時(shí)候來(lái)執(zhí)行。
這里的“空閑”主要指兩種情況
(1)消息隊(duì)列為空
(2)消息隊(duì)列不為空但全部是延時(shí)消息,也就是 msg.when > now
利用這一特性,我們可以將一些不重要的初始化操作放在 IdleHandler 中執(zhí)行,以此加快 app 啟動(dòng)速度;由于 View 的繪制是事件驅(qū)動(dòng)的,我們也可以在主線程的事件循環(huán)中添加一個(gè) IdleHandler 來(lái)作為 View 繪制完成的回調(diào),等等。 但應(yīng)該注意的是,如果主線程中一直有任務(wù)執(zhí)行,IdleHandler 被執(zhí)行的時(shí)機(jī)會(huì)無(wú)限延后,使用的時(shí)候要注意哦~
本篇是【面試官爸爸】系列第三篇,后續(xù)我還會(huì)繼續(xù)更新這個(gè)系列,包括面試最??嫉?Activity 啟動(dòng),編譯打包流程及優(yōu)化,Java 基礎(chǔ),設(shè)計(jì)模式,組件化等面試常問(wèn)的問(wèn)題。如果不想錯(cuò)過(guò),歡迎點(diǎn)贊,收藏,關(guān)注我!球球兄弟萌辣,這個(gè)對(duì)我真的很重要!!
我是方木
一個(gè)在互聯(lián)網(wǎng)世界掙扎向上的打工人
努力生活,努力向前
微信搜公眾號(hào) 方木 Rudy 第一時(shí)間獲取我的更新!里面不僅有技術(shù),還有故事和感悟
搜索它,帶走我!我們下期見~