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的消息機制都會非常輕松的面對!!!