關於Handler你所需要知道的一切

2021-03-02 秦子帥


簡書:https://www.jianshu.com/p/3523bed4d88f前言

Handler是個老生常談的問題,我相信幾乎所有的Android開發者都會使用Handler,那關於Handler還有什麼好講的嗎?Handler如果僅僅是使用的話,確實沒什麼好講的,但是Handler卻是一個幾乎所有面試官都會問的問題,不同的要求問的深度也不一樣,今天我就帶大家學習一下關於Handler你所必須要掌握的知識。

Handler消息機制

首先有四個對象我們必須要了解一下HandlerLooperThreadLocal還有MessageQueue

Handler

首先我們要明白Handler消息機制是用來幹嘛的?Handler是把其他線程切換到Handler所在的線程,注意,這裡不是UI線程。雖然我們的大多數使用Handler的場景,都是我們在子線程做了一下耗時的操作(IO或者資料庫),當子線程執行完以後我們可能需要更新UI,這個時候我們用Handler來處理(sendMessage()或者post())就把子線程切換到了UI線程了。假如說,我們現在有這麼一個需求,線程A發個信息給線程B(線程A、線程B都不是主線程),這個時候我們用Handler依然可以做,只需要在線程B中創建好Handler就可以了(關於如何在子線程創建Handler我會在下面詳細說明)。所以,Handler並不是把其他線程切換到主線程(UI線程),而是切換到它所在的線程,這一點一定要弄清楚。

Looper

Handler消息機制裡面最核心的類,消息輪詢。Handler要想切換線程(或者說發送消息),必須要先獲得當前線程的Looper,然後調用Looper.loop()方法把MessageQueue裡面的message輪詢出來"交"給Handler來處理,至於如何「交」給Handler的,下面我會通過源碼帶大家分析。

ThreadLocal

ThreadLocal是Looper內部的一個,它雖然前面有個「Thread」,但其實它並不是線程,它相當於一個線程內部的存儲類。剛才在講Looper的時候我們說到,「Handler要想切換線程(或者說發送消息),必須要先獲得當前線程的Looper」,那如何獲得當前線程的Looper呢?正是通過ThreadLocal,它可以在不同線程之間互不幹擾地存儲數據。ThreadLocal裡面有一個內部靜態類對象ThreadLocalMap,通過其set()和get()方法對數據對象進行保存和讀取。

MessageQueue

MessageQueue——消息隊列,雖然它叫消息隊列,但實際上的結構並不是一個隊列,而是一種單鍊表的數據結構,只是使用列隊的形式對數據進場做添加或者移除。MessageQueue是在Looper被創建的時候由Looper生成的。同一線程下,Handler發送的所有message都會被加入到MessageQueue裡面,當Message被loop()以後,就會從消息隊列中移除。Message我沒有單獨拿出來,因為確實比較簡單,就是消息本身,它可以攜帶兩個int型參數,如果要傳比較複雜的參數就用obj屬性,what屬性用來區分是哪個Handler發送的信息。

小結一下

Handler是把其他線程切換到它所在的線程,使用Handler發消息之前要先創建Looper對象,創建和讀取Looper都需要使用ThreadLocal,它可以在不同線程之間互不幹擾地保存數據。在創建Looper對象的同時也把MessageQueue創建好了,Handler所發的message會被添加到MessageQueue對象裡面,Looper.loop()方法以後會把MessageQueue上的Message輪詢出來。連接這幾個對象的還有一條很重要的線,那就是線程,記住,以上的過程都要保證Handler、Looper、MessageQueue在同一線程,這樣,才能保證Handler的消息機制被正確使用。

子線程創建Handler

注意:我們這裡是為了讓大家更好地學習Handler,所以要在子線程創建Handler,現實使用場景,很少會在子線程創建Handler

在主線程(UI線程)創建Handler,我相信所有人都會

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainHandler = new Handler();
mainHandler.post(new Runnable() {
@Override
public void run() {
Log.e("qige_test", "thread_name=" + Thread.currentThread().getName());
}
});
}


我們先按照主線程的方式在子線程創建一個Handler試一下,看看會有什麼樣的結果

new Thread(){
@Override
public void run() {
childHandler=new Handler();
childHandler.post(new Runnable() {
@Override
public void run() {
Log.e("qige_test","thread_name="+Thread.currentThread().getName());
}
});

}
}.start();


沒錯,如圖所示還沒有創建Looper,那麼如何創建Looper呢?圖中有句話已經給出了答案——Looper.prepare(),同時為了讓我們發送的消息被輪詢到,還必須要調用Looper.loop(); 所以在完整的在子線程創建Handler的代碼如下:

new Thread(){
@Override
public void run() {
//創建Looper,同時創建MessageQueue
Looper.prepare();

childHandler=new Handler();
childHandler.post(new Runnable() {
@Override
public void run() {
Log.e("qige_test","thread_name="+Thread.currentThread().getName());
}
});

//輪詢
Looper.loop();

}
}.start();


這樣就可以在子線程裡面創建Handler了,看到這裡,你可能會問,為什麼我在主線程創建Handler的時候,沒有調用Looper.prepare()和Looper.loop()?這個問題我們先放一下,我們先從源碼角度把Handler消息機制分析一下。

從源碼分析

先看一下創建Looper和MessageQueue的方法Looper.prepare()

public static void prepare() {
prepare(true);
}

private static void prepare(boolean quitAllowed) {
//ThreadLocal來了
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

這裡就出現了我剛才說的ThreadLocal對象。Android規定每個線程有且僅有一個Looper,所以為了防止不同的線程之間的Looper相互影響,使用ThreadLocal對象來存儲Looper。

再看一眼new Looper()的源碼

private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

MessageQueue也出來了,創建Looper的同時創建MessageQueue。

下面我們看Handler.post()或者是Handler.sendMessage()他們本質是一樣的,把消息加入到消息隊列當中

public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}


post()或者sendMessagexxx()最終都會調用sendMessageAtTime(Message msg, long uptimeMillis)方法,我們直接看這個方法的源碼

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
//關鍵代碼部分
return enqueueMessage(queue, msg, uptimeMillis);
}


都很簡單,最後一步enqueueMessage(queue, msg, uptimeMillis);是把消息加入隊列,咱們再點開看一下

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
//把當前的Handler賦值給msg.target
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

關鍵要看的地方是 msg.target = this剛才講Message的時候沒有提,Message的target屬性就是一個Handler對象,這裡直接把當前的Handler對象賦值過去了。最後一行:queue.enqueueMessage(msg, uptimeMillis)是把message加入到消息隊列裡面,具體的實現我抓不到了,知道這句話是把message加入到MessageQueue裡面就行。
下面分析* Looper.loop()  方法的關鍵部分源碼

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
**標註1**
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
....
..
**標註2**
for (;;) {
Message msg = queue.next();
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
.

try {
**標註3**
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}


**標註4**
msg.recycleUnchecked();
}
}

看源碼的時候注意看一下標註,一個個地來講;

標註1:保證了Handler、Looper和MessageQueue在同一線程

標註2:沒有看錯,就是一個無線死循環,Looper會不停地對MessageQueue進行輪詢。這時候,你可能會問,這種無限死循環會不會很浪費資源?其實並不會,因為當沒有message被添加到隊列的時候,程序會進入阻塞狀態,對資源的消耗是很小的。

標註3:還記得剛才講地msg.target吧,這裡 msg.target.dispatchMessage(msg);就是handler.dispatchMessage(msg),直接去看Handler的dispatchMessage(msg)方法

public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

這裡面msg.callback是個Runnable對象,也就是當我們使用handler.post(Runnable r)的方法的話,這時候就直接去調用Runnable對象裡面的東西了,如果使用的是handler.sendMessagexxx(),就是去調用我們重寫的handleMessage(msg)方法了。
如果調用post()方法發送消息

mainHandler.post(new Runnable() {
@Override
public void run() {
//這裡會被調用
Log.e("qige_test","thread_name="+Thread.currentThread().getName());
}
});

如果調用sendMessagexxx()

mainHandler.sendEmptyMessage(0);
mainHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
//這裡會被調用
Log.e("qige_test","what="+msg.what);
}
};

我們再回到上一級源碼

以上基本就是一個完整的Handler消息機制的過程,我再帶大家好好回顧一下:

1.Looper.prepare();//這一步創建了Looper和MessageQueue
2.handler.sendMessagexxxx(); // 把message添加到MessageQueue上
3.Looper.loop();//輪詢消息
4.handler.dispatchMessage(msg);//處理消息

關於在主線程創建Handler的疑問

好了,到了該解決歷史遺留問題的時候了,為什麼我們在主線程創建handler不需要調用Looper.prepare()和Looper.loop()呢?
這是因為,主線程已經給我們創建好了,在哪裡創建好的呢?
在Java裡面,有一個程序的主入口,就是靜態main方法

public class Test {
//程序入口
public static void main(String... args){

}
}

在Android裡面呢,同樣有這麼一個main方法入口,它在ActivityThread類中。在我們App啟動過程中,會調用到ActivityTread的main()方法(由於篇幅問題,該過程沒有辦法詳細講,後期會推文章單獨說一下),下面我們直接看ActivityTread的main()方法的源碼
為了方便理解,還是只看關鍵部分代碼

public static void main(String[] args) {
....

//創建Looper和MessageQueue對象,用於處理主線程的消息
Looper.prepareMainLooper();

//創建ActivityThread對象
ActivityThread thread = new ActivityThread();

//建立Binder通道 (創建新線程)
thread.attach(false);

//消息輪詢
Looper.loop();

}

看到這裡,我想你應該明白了為啥在主線程裡創建Handler不需要調用Looper.prepare()和Looper.loop()方法了吧,因為在App啟動的時候,ActivityThread已經給我們創建好了。不過,需要注意的是,我剛才在講Looper.loop()源碼的時候說過這裡面會有一個無限死循環,那麼程序運行到這裡豈不是要永遠卡在這了呢,那我們的Activity、Service都是咋執行的呢?看關鍵的源碼這一句thread.attach(false),注釋上其實解釋的很清楚,通過Binder創建了新的線程,在這個新的線程裡面運行了Activity、Service等組件,而之所以Android為什麼採用這種方式,讓我們留在以後再說,你只需要知道,Looper.loop()不會造成卡頓,主線程已經給我們創建好了Looper和MessageQueue對象就可以了。

為什麼寫這篇文章

開篇我就說過,Handler幾乎所有人都會用,但僅僅會用是不夠,要知其然更知其所以然。很多面試官願意問Handler相關的問題,好好閱讀這篇文章,它會讓你在面試的時候事半功倍,希望對大家有所幫助,點個讚!

     

   創作不易,點個「在看

相關焦點

  • 你真的了解Handler嗎
    今天發一個以前的文章,關於Handler的全面解析,大家看看吧~「周末愉快」!❝提到handler,大家都想到些什麼呢,切換線程?延時操作?那麼你是否了解「IdleHandler,同步屏障,死循環」的設計原理?
  • 關於焦慮你需要知道的一切
    關於焦慮你需要知道的一切概述焦慮是你身體對壓力的自然反應。這是一種對即將發生的事情的恐懼或憂慮。開學的第一天,去參加一個工作面試,或者發表演講,可能會讓大多數人感到恐懼和緊張。了解更多關於每一個的信息,這樣你就可以判斷你的症狀是否是由其中一個引起的。什麼引起焦慮?研究人員不確定焦慮的確切原因。但是,這很可能是多種因素共同作用的。這些因素包括遺傳和環境因素,以及大腦化學。此外,研究人員認為,負責控制恐懼的大腦區域可能受到影響。目前對焦慮的研究正在深入研究大腦中與焦慮有關的部分。
  • 關於溝通,你所需要知道的一切都在這裡
    ▶ 他們可能不知道你為什麼會拒絕他。如果這個活動他可以在當天晚些時候,或本周另一天做,嘗試在視覺時間表上顯示出來。▶ 「不要/不行」通常會在危險的情況下使用,如果這是關於人身安全的問題,嘗試向他解釋什麼是危險與安全。
  • 關於假髮你需要知道的一切
    關於假髮你需要知道的一切「轉」Everything you need to know about wigs所以,現在似乎是時候召集髮型專家來提供關於假髮的終極指南了,從哪裡可以買到假髮,到尋找人類頭髮的道德標準,到如何將假髮戴在你的頭上以及後續護理... ..。
  • 加拿大醫療丨關於魁北克醫療卡,你所需要知道的一切!
    (分享嘉賓:coco)這篇文章要跟大家分享的是在魁省的醫療保險卡,也就是太陽卡,你所應該知道的一切。很多人選擇來加拿大,我相信免費的全民醫療保險制度是大家看中的其中一個原因。還有就是以上所說的這些人員,他們的配偶和子女都可以同樣拿到太陽卡。
  • Android開發:HandlerThread是什麼?
    只要有過Android開發經驗的同學都知道,在Android中,執行耗時任務的場景特別常見,比如圖片資源的下載、資料庫的增刪改查、文件IO等。為了保證APP的流暢性,這些耗時操作一般都需要放入子線程處理。這裡做法就比較多了,也是體現程式設計師級別的地方。最簡單粗暴的方法就是每次new出一個Thread去執行耗時任務,執行完任務這個線程也就銷毀了,這種做法是最不應當採取的,消耗太大。
  • 面試必備:異步 Handler 十大必問!
    Handler 的基本原理關於 Handler 的原理,相比不用多說了,大家都應該知道,一張圖就可以說明(圖片來自網絡)。2. 子線程中怎麼使用 Handler除了上面 Handler 的基本原理,子線程中如何使用 Handler 也是一個常見的問題。
  • 關於面試化妝你需要知道的一切.
    人對對方的社會知覺例如可信度、強勢程度和吸引力很多時候往往就在接觸到你的100微秒內就能形成。曾經看過一個關於面相的研究報導,發現人對陌生人給出的印象評價中58%的差異都和生理特徵有直接關係和對應關係。
  • Handler 使用詳解
    和你一起終身學習,這裡是程式設計師Android
  • 背雅思單詞你所需要知道的一切
    這個道理我想大家都懂,但我們就是不願意去背單詞,想到單詞就頭疼,背了就忘,看著眼熟卻不知道意思,知道意思卻不會用,即使會用這個詞了,口語和寫作中總是想不起來用,對嗎?知乎大V尼克六六給我們分享了他關於單詞學習的一切,一起來看看:
  • 關於Android 進程保活,你所需要知道的一切
    關於 Android 平臺的進程保活這一塊,想必是所有 Android 開發者矚目的內容之一。你到網上搜 Android 進程保活,可以搜出各種各樣神乎其技的做法,絕大多數都是極其不靠譜。前段時間,Github還出現了一個很火的「黑科技」進程保活庫,聲稱可以做到進程永生不死。
  • 談--關於復讀的一切所需要考慮的問題
    每個人對於復讀的初衷是不一樣的,我是想考上本科,我同學當中有考上本一的,想去985;本二的,想去本一;專科的,想去本科;還有連專科都沒考上的,想去考專科;還有家長逼孩子去讀的,孩子想再「讀」(其實是玩)一年的...等等等等,很多不同的想法和初衷,就這樣去了復讀,那麼我現在,對於今年準備去復讀的同學,先梳理一下關於復讀的一些事情--首先,關於復讀的目的
  • Android中Handler問題匯總【面試必備】
    源碼,但是有些面試官問的問題卻不一定能夠回答出來,趁著機會總結一下面試中所覆蓋的handler知識點。2.Handler機制,sendMessage與post(Runable)的區別要看sendMessage和post區別,需要從源碼來看,下面是幾種使用handler的方式,先看下這些方式,然後再從源碼分析有什麼區別。
  • 關於新電影《小婦人Little Women》你所需要知道的一切
  • Android開發 面試必問的Handler消息機制
    RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); }這裡如果有疑問說ThreadLocal是什麼東西,其實ThreadLocal是線程用來存儲數據的Map,咱們看一下set方法源碼就知道了
  • 關於物理學你需要知道的一切
    文章很長, 智力不足者不遠送~▎關於物理學你需要知道的一切 特別註明:本文素材主要摘錄自加來道雄的在這個時空中你要準確標定一個點,需要四個數字,比如:淮海路、馬當路交界,二樓,下午四點。) 依此類推,,, 如果,有人告訴你,多維空間(四維以上)的人,看著我們的保險柜或房子時根本就是內外一目了然,就像我們看著二維平面上的圓圈一樣,你會不會大惑不解、無法想像、或者毛骨悚然?
  • Handler原理,這一篇就夠了
    前言hanlder的大概原理,可能很多人不知道,至少不清楚,網上很多文章也是到處粘貼,聽別說handler把Message發送到MessageQueue裡面去,Looper通過死循環,不斷從MessageQueue裡面獲取Message處理消息,因為Mesage.target就是當前hanlder,所以最後轉到handleMessage()方法中去處理,整個流程是這樣。
  • 5分鐘了解Handler錯誤使用場景
    希望對你有一點幫助。  來,咱們進入角色。  Hander,Looper,MessageQueue,Message的全程協作的關係就好比一個餐廳的整體運作關係:  Handler —— 點餐員Looper —— 後廚廚師長。MessageQueue —— 訂單打單機。
  • 關於在家裡染髮你需要知道的一切
    考慮一下你的頭髮的質地。頭髮的質地,或者說多孔性,會影響到你選擇的顏色在頭髮上形成的顏色是亮是暗。「你需要確保你的頭髮從髮根到發梢都有均勻的孔隙度,以確保你的發梢不會變得更深,」Lee解釋道。「如果你的頭髮末端是幹的,在塗顏色之前,你可以在發梢噴上一層淡色護髮素,這樣就可以消除頭髮的氣孔」。6。正確地準備。
  • Android Handler 由淺入深源碼全解析
    關於 handler的源碼已經很前人分享過了。如果我沒能給大家講明白可以參考網上其他人寫的。        註:文中所貼源碼都有精簡過,並非完整源碼,只保留主思路,刪減了一些非法校驗細節實現目錄簡單使用方法        應用層開發時handle常要用於線程切換調度和異步消息、更新UI等,但不僅限於這些。