Android Handler機制徹底梳理


Android的消息機制其實也就是Handler相關的機制,對於它的使用應該熟之又熟了,而對於它的機制的描述在網上也一大堆【比如15年那會在網上抄了一篇https://www.cnblogs.com/webor2006/p/4837623.html對它的關系描述,但僅僅是背一背概念】,在面試時也時不時的會問起它,說實話從事Android這么多年也沒自己從頭到尾的去將它的工作機制詳細的給挼一遍,所以這里寫一篇關於它的整個機制的描述來加深對Handler的核心機制的進一步了解。

Android消息機制:

先來看一張關於整個消息機制的描述圖,這個流程會在之后自己從0開始手寫實現的,如下:

以上模型大致解釋一下:

1、以Handler的sendMessage方法為例,當發送一個消息后,會將此消息加入消息隊列MessageQueue中。
2、Looper負責去遍歷消息隊列並且將隊列中的消息分發給對應的Handler進行處理。
3、在Handler的handleMessage方法中處理該消息,這就完成了一個消息的發送和處理過程。
這里從圖中可以看到參與消息處理有四個對象,它們分別是 Handler, Message, MessageQueue,Looper。

其中在圖中涉及到這兩個狀態:

這個在之后的源碼分析中是能看到的。

ThreadLocal的工作原理:

ThreadLocal 是一個線程內部的數據存儲類,通過它可以在指定的線程中存儲數據,數據存儲以后,只有再指定線程中可以獲取到存儲的數據,對於其他線程來說則無法獲取到數據。為啥要先說它呢?因為打好這個基礎后有助於分析Android的消息機制,下面先舉例來對它的工作原理有一個了解:

先來瞅一下ThreadLocal的源碼:

接受一個泛型,那該泛型是怎么用的呢?

它里面有一個ThreadLocalMap的靜態內部類,然后在我們往ThreadLocal存東西時最終的這個T會賦值給ThreadLocal中的Entry類中的value,如下:

 

 

下面來定義一下:

下面來調用一下:

此時看一下這個get()方法的實現:

接下來根據當前線程來獲取ThreadLocalMap:

 

其中可以看一下該threadLocals在Thread初始化的情況:

所以。。回到get()主流程上來:

然后:

最后setInitialValue()方法就會返回cexo,然后整個get()方法就返回了:

這就是為啥我們的結果顯示cexo的原因,好,下面將在子線程中再來打印一下:

其原因就不多分析了,跟在主線程的是一模一樣的流程,都是由於我們重寫了initialValue()方法。 

下面再來看:

此時再來分析一下set的過程:

然后再拿時:

下面再來改一下程序,再改之前需要將ThreadLocal中設置的東東給清除掉,避勉內存泄漏,如下:

好,再來新建一個線程:

也就是通過ThreadLocal存放的值是跟線程綁定的,關於它的大致使用就了解到這,下面正式進入到核心的消息機制源碼分析階段了。

Android消息機制源碼分析:

1、啟動App創建全局唯一得Looper對象和全局唯一得MessageQueue消息對象:

先來看一下整體這塊的流程圖:

以此為藍本,接下來分析一下這塊流程的源碼【以Android9.0為例】:

點擊進入看一下:

看到了ThreadLocal的身影了,這就是為啥要先了解它的機制的原因之所在,好,此時流程就到了這:

然后看一下這個Looper創建的細節,就會創建全局唯一的消息隊列,如下:

以上就是在主線程啟動時創建Looper的大致過程。

2、Activity中創建Handler:

先貼一下整個這塊的流程:

下面先回顧一下它的實際使用,比較簡單,主要是根據實際的應用來過行源碼底層分析會比較親切:

 

先來看一下Handler()的構造:

 

3、發送消息:

整體流程:

咱們依照此流程來分析一下:

繼續往里跟:

流程就跑到了這:

接下來來看一個這個enqueueMessage()方法:

其中可以看一下Message.target變量:

也就是每個消息都綁定了Handler,下面回到主流程:

也就是如流程圖的這一步:

具體看一下在全局消息隊列中的處理:

 呃,貌似有點顛覆對隊列的認知,不應該拿到消息往隊列中插么,貌似這里就是簡單的給它里面的成員變量賦了個值而已呢,是的,這個消息隊列一定得要知道並非是我們認知中的那種,下面看一眼它的javadoc對它的描述:

以上是消息發送的大致流程。

4、處理消息:

先來看一下這塊的大致流程圖:

依據它再來看一下代碼流程:

下面具體來看一下該loop()方法,首先也是獲取全局唯一的Looper和MessageQueue對象:

接着則會循環從隊列中取消息,將會調用消息隊列綁定的Handler的相關的方法來對消息進行處理,如下:

那咱們再來看一下Handler中的這個消息分發是如何來處理該消息的:

 

消息阻塞和延時:

Looper 的阻塞主要是靠 MessageQueue 來實現的,在next()@MessageQuese 進行阻塞,在 enqueueMessage()@MessageQueue 進行喚醒。主要依賴 native 層的 Looper 依靠 epoll 機制 進行的。
nativePollOnce(ptr, nextPollTimeoutMillis); 這里調用naive方法操作管道,由nextPollTimeoutMillis決定是否需要阻塞
nextPollTimeoutMillis為0的時候表示不阻塞,為-1的時候表示一直阻塞直到被喚醒。

消息阻塞流程:

下面具體來看一下代碼,先看在Looper中的loop()的阻塞相關:

接着則看MessageQueue的next()方法了:

那假如在這塊阻塞了之后,那在主線程中不會引發ANR么?其實是不會的,原因簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,通過往pipe管道寫端寫入數據來喚醒主線程工作。這里采用的epoll機制,是一種IO多路復用機制。

好,下面再詳細的來分析其阻塞的一個整體流程:

假設loop for循環第一次、MessageQueue for循環也第一次:

 

接着就會往下執行,則會執行到這:

好繼續:

 

假如它現在等於0,則會執行到這了:

 此時再下一次循環中,則就會進入阻塞狀態了:

而假如mIdleHandlers.size()>0,那么執行順序就會發生變化了,如下:

 

好,接下來再假設mMessages不為null:

當退出循環時,如果找到了,則走如下條件分支:

下面具體再看下里面的條件:

而這個流程往下:

這就是消息延時的一個機制。 

消息延時入隊流程:

此時就需要回到MessageQueue中的enqueueMessage()方法了,如下:

而如果當前消息的when要大於上一條消息的when,則會走另一個條件分支了,如下:

先來看一下javadoc的說明:

而在理解這個分支代碼之前需要理解一個東東:

對於Message對像池的大小是有大小的,那是多少呢?下面看下源碼的定義:

 

那如果消息大小超過了這個對象池總個數呢,則是插不進去的,具體這塊的代碼如下:

 

其中sPool是一個靜態變量:

了解了它的對象池之后,下面再回過頭來理解這個條件:

比如上一個消息為:

然后再插入個新消息為:

此時進入條件時:

也就是處理完之后就成這樣了:

同樣的如果再來第三個msg:

同樣的也會按從小到大的順序來進行排序:

 

最后則會執行喚醒的條件,如下:

 

當執行喚醒時,則在next()中正在阻塞的就會被喚醒:

手寫Handler消息核心機制:

經過這么大的篇幅來對Handler核心流程的源碼進行了分析之后,接下來弄一個比較有“挑戰”的事,從0開始手寫一上handler發送及消息接收的“核心流程”,不涉及到延時相關的東東,因為那塊太復雜了,下面從ActivityThread.main()中一直到Activity創建消息到接收消息手寫實現一下,這里拋開Android環境以單元測試的方式來手寫,先定義一個main()方法:

來模擬它:

然后我們先將Looper、Handler、MessageQueue、Message都創建一下:

好,在main()中首先得生成全局唯一的Looper對象,如下:

 

接下來實現這個方法:

校仿一下:

然后在Looper的構造方法中需要初始化MessageQueue,如原碼中所示:

所以繼續校仿:

好,接下來再來創建Handler,如這個流程:

它里面持有Looper和MessageQueue的引用,如系統源碼所示:

所以咱們在我們的構造方法中來實例化一下:

然后來在MyLooper中實現myLooper()方法,還是校仿源碼:

然后再來實例化MessageQueue:

然后我們在主線程中來創建一個Handler:

我們知道在實際使用時需要重寫它里面的一個handleMessage()方法,所以咱們還得在MyHandler中來定義一下該方法,如下:

此時就可以重寫方法了:

好,接下來在子線程中來發送消息,如這個流程:

其中消息里面得要有一些屬性,這里只定義簡單的幾個,如下:

然后咱們繼續來創建消息:

此時咱們再來定義發送消息的方法,先看一下源碼是如何寫的:

所以咱們也來寫一下:

我們之前分析過MessageQueue的入隊方法,它是采用對像池的方式來存儲的,咱們這里簡單一點,直接用阻塞隊列來存放,重在模擬整個過程,如下:

好,最后就是開始Looper的消息循環了,如源碼所示:

接下來這就是最后一步的實現了,下面來實現一下:

好,接下來則來看一下這個next()方法的實現:

這里就木有實現阻塞隊列,還是重點看整體流程,好繼續,先看一下系統源碼,當拿到消息之后接下來是怎么處理的:

所以咱們校仿一下:

接下來就要分發消息的處理了,還是先來參照系統的方式:

 

所以咱們簡單處理一下:

修改一下:

好,接下來就到最后的消息處理啦,如下:

Handler中常見問題分析:

  • 為什么不能在子線程中更新UI,根本原因是什么?

    mThread是UI線程,這里會檢查當前線程是不是UI線程。那么為什么onCreate里面沒有進行這個檢查呢。這個問題原因出現在Activity的生命周期中,在onCreate方法中,UI處於創建過程,對用戶來說界面還不可視,直到onStart方法后界面可視了,再到onResume方法后界面可以交互。從某種程度來講,在onCreate方法中不能算是更新UI,只能說是配置UI,或者是設置UI的屬性。這個時候不會調用到ViewRootImpl.checkThread(),因為ViewRootImpl沒被創建。而在onResume方法后,ViewRootImpl才被創建。這個時候去交互界面才算是更新UI。
    setContentView只是建立了View樹,並沒有進行渲染工作(其實真正的渲染工作是在
    onResume之后)。也正是建立了View樹,因此我們可以通過findViewById()來獲取到View對象,但是由於並沒有進行渲染視圖的工作,也就是沒有執行ViewRootImpl.performTransversal。同樣View中也不會執行onMeasure(),如果在onResume()方法里直接獲取View.getHeight()/View.getWidth()得到的結果總是0。

  • 為什么主線程用Looper死循環不會引發ANR異常?

    簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在loop的queue.next()中的nativePollOnce()方法里,
    此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,
    通過往pipe管道寫端寫入數據來喚醒主線程工作。這里采用的epoll機制,是一種IO多路復用機制。

  • 為什么Handler構造方法里面的Looper不是直接new?
    如果在Handler構造方法里面new Looper,怕是無法保證保證Looper唯一,只有用Looper.prepare()才能保證唯一性,具體去看prepare方法。
  • MessageQueue為什么要放在Looper私有構造方法初始化?
    因為一個線程只綁定一個Looper,所以在Looper構造方法里面初始化就可以保證mQueue也是唯一的Thread對應一個Looper 對應一個 mQueue。
    談到這點,發現咱們手寫的代碼中關於Looper的構造定義不對,當時是定義成了public了,如下:

    而系統中定義確實是私有的:

    所以修改一下:

  • Handler.post的邏輯在哪個線程執行的,是由Looper所在線程還是Handler所在線程決定的?
    由Looper所在線程決定的。邏輯是在Looper.loop()方法中,從MsgQueue中拿出msg,並且執行其邏輯,這是在Looper中執行的,因此由Looper所在線程決定。
  • MessageQueue.next()會因為發現了延遲消息,而進行阻塞。那么為什么后面加入的非延遲消息沒有被阻塞呢?
    這是因為新消息在入列時,會存在喚醒的情況,如下:
  • Handler的dispatchMessage()分發消息的處理流程?

    Msg.callback 在mHandler1.post()中使用
    mCallback在new Handler是通過接口回調

    Post()和sendMessage()都是發送消息,加入消息隊列得方式也是一樣,區別在於處理消息得方式。通過跟蹤源碼,容易區分。

終於。。整個Handler相關的東東都梳理完了,真的,還是細節挺多的,不過這么走了一遍真的受益匪淺!!如果把整個全部消化,我想未來不管面試官怎么來問Android的消息機制都會非常輕松的面對!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM