【Android自助餐】Handler訊息機制完全解析(二)MessageQueue的佇列管理
Android自助餐Handler訊息機制完全解析(二)MessageQueue的佇列管理
關於這個佇列先說明一點,該佇列的實現既非Collection的子類,亦非Map的子類,而是Message本身。因為Message本身就是連結串列節點(見Message中obtain()與recycle()的來龍去脈)。
佇列中的Message mMessages;
成員即為佇列,同時該欄位直接指向佇列中下一個需要處理的訊息。
新增到訊息佇列enqueueMessage()
要將message新增到佇列除了提供message之外,還需提供訊息觸發時間when
。
如果當前佇列為空則直接mMessage=message即可。否則就需要逐個對比佇列中每個message的when和新訊息的when來確定新訊息在佇列中的位置。
先給出核心原始碼(有刪減)
Message p = mMessages;
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
} else {
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
}
msg.next = p;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
先看下新訊息需要放到隊頭的情況:p == null || when == 0 || when < p.when
。即佇列為空,或者新訊息需要立即處理,或者新訊息處理的事件比隊頭訊息更早被處理。這時只要讓新訊息的next
指向當前隊頭,讓mMessages
指向新訊息即可完成插入操作。
除了上述三種情況就需要遍歷佇列來確定新訊息位置了,下面結合示意圖來說明。
假設當前訊息佇列如下
開始遍歷:p向隊尾移,引入prev指向p上一個元素
假設此時p所指訊息的when比新訊息晚,則新訊息位置在prev與p中間
最後便是呼叫native方法來喚醒(Linux的epoll,有興趣的自行百度)。
從佇列取出訊息next()
這部分內容有點高能,請根據個人BPU(BrainProcessUnit)酌情理解。
首先這個方法需要返回Message
,那麼我們現在來看看哪裡有return
。(共三段,我們最後看第二段。)
第一段
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
如果mPtr為0則返回null。那麼mPtr是什麼?值為0又意味著什麼?在MessageQueue
構造方法中呼叫了native方法並返回了mPtrmPtr = nativeInit();
;在dispose()
方法中將其值置0mPtr = 0;
並且呼叫了nativeDestroy()
。而dispose()
方法又在finalize()
中被呼叫。另外每次mPtr的使用都呼叫了native的方法,其本身又是long型別,因此推斷它對應的是C/C++的指標。因此可以確定,mPtr
為一個記憶體地址,當其為0說明訊息佇列被釋放了。這樣就很容易理解為什麼mPtr==0
的時候返回null了。
第三段
你沒有看錯,第二段在後面
if (mQuitting) {
dispose();
return null;
}
這裡的意思也很明顯,當這個訊息佇列退出的時候,返回空。而且在返回前呼叫了dispose()
方法,顯然這意味著該訊息佇列將被釋放。
第二段
這部分涉及到的程式碼基本上就是這個next()
方法本身了,但可以肯定的是這裡的返回語句是return msg;
。同時從enqueueMessage()
方法可以看出來,在這個佇列中取到的message物件不可能為空,因此這裡的返回絕對不為空。
如此一來就可以得出一個結論:如果next()
方法為空說明這個訊息佇列正在退出或將被釋放回收。
繼續來看這個next()
,這個程式碼有點長,所以先做個減法。
第一個要減的就是pendingIdleHandlerCount
,這個區域性變數初始為-1,後面被賦值mIdleHandlers.size();
。這裡的mIdleHandlers
初始為new ArrayList<IdleHandler>()
,在addIdleHander()
方法中增加元素,在removeIdleHander()
方法中移除元素。而我們所用的Handeler
並未實現IdleHandler
介面,因此在next()
方法中pendingIdleHandlerCount
的值要麼為0,要麼為-1,因此可以看出與該變數相關的部分程式碼執行情況是確定的,好的,把不影響迴圈控制的程式碼減掉。
第二個要減的是Binder.flushPendingCommands()
這個程式碼看原始碼說明:
Flush any Binder commands pending in the current thread to the kernel driver. This can be useful to call before performing an operation that may block for a long time, to ensure that any pending object references have been released in order to prevent the process from holding on to objects longer than it needs to.
這段話啥意不懂也沒關係,這裡只需要知道:Binder.flushPendingCommands()
方法被呼叫說明後面的程式碼可能會引起執行緒阻塞。然後把這段減掉。
第三個要減的是一個log語句if (DEBUG) Log.v(TAG, "Returning message: " + msg);
第四個要減的是上面提到的“第一段”返回null的語句,但是“第三段”得留著。
最後再把註釋幹掉給上程式碼:
Message next() {
int nextPollTimeoutMillis = 0;
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}
if (mQuitting) {
dispose();
return null;
}
if (pendingIdleHandlerCount <= 0) {//上面分析過該變數要麼為0要麼為-1
mBlocked = true;
continue;
}
}
nextPollTimeoutMillis = 0;
}
}
雖然還是很長,但也不能再減了。大致思路如下:先獲取第一個同步的message。如果它的when
不晚與當前時間,就返回這個message;否則計算當前時間到它的when
還有多久並儲存到nextPollTimeMills
中,然後呼叫nativePollOnce()
來延時喚醒(Linux的epoll,有興趣的自行百度),喚醒之後再照上面那樣取message,如此迴圈。程式碼中對連結串列的指標操作佔了一定篇幅,其他的邏輯很清楚,就不一句句分析了。
從佇列移除訊息removeMessages()
該方法有2個過載,除此之外還有removeCallbacksAndMessages()
等方法也可以移除訊息。但程式碼段都基本一樣,這裡以void removeMessages(Handler h, int what, Object object){}
方法為例。
該方法完整原始碼如下
void removeMessages(Handler h, int what, Object object) {
if (h == null) {
return;
}
synchronized (this) {
Message p = mMessages;
// Remove all messages at front.
while (p != null && p.target == h && p.what == what
&& (object == null || p.obj == object)) {
Message n = p.next;
mMessages = n;
p.recycleUnchecked();
p = n;
}
// Remove all messages after front.
while (p != null) {
Message n = p.next;
if (n != null) {
if (n.target == h && n.what == what
&& (object == null || n.obj == object)) {
Message nn = n.next;
n.recycleUnchecked();
p.next = nn;
continue;
}
}
p = n;
}
}
}
最開始判斷handler是否為空不必多說,然後便是同步程式碼段,只裡面有兩個while迴圈。為什麼有兩個呢?學過資料結構連結串列的都知道,連結串列分兩種:帶頭結點和不帶頭結點。而這兩種連結串列的遍歷方式有所不同:不帶頭結點的連結串列中,第一個元素需要單獨處理,然後才能將後續部分當做帶頭結點的連結串列來使用while迴圈遍歷。可以看出MessageQueue是不帶頭結點的連結串列,而且遍歷過程中有需要刪除節點,因此要特殊處理的不只是第一個元素,而是第一組符合刪除條件的元素。有點暈了是吧,不要緊,我們開始鬥圖。
第一個while
假設需要遍歷的訊息佇列如圖所示。
為了讓第一個while可以執行,我們假設前3個元素符合移除條件,即前三個Message的targe
、what
、obj
分別與指定的handler
、what
、object
相同。首先第一個元素滿足條件進行如下操作:
執行n=p.next;
後移mMessage;
回收p指向的元素,即第一個元素。
讓p指向新的隊頭。
此時又與初始佇列狀態一樣了。先前我們假設隊頭有三個元素符合移除條件,因此再迴圈執行上面4圖2邊後又得到初始狀態的佇列,此時隊頭元素不滿足移除條件因此while終止,同時新的佇列變成了“帶頭結點的連結串列”,因此mMessage指向的元素永遠不用被判斷是否滿足移除條件。
第二個while
此時訊息佇列狀態如下:
執行n=p.next;
假設n指向的元素不滿足移除條件,則只需要將p和n後移,如此也說明,p指向的元素總是已經被判斷過不滿足移除條件的。這部分邏輯很簡單到給圖就是看不起讀者的智商,現在我們假設n指向的元素滿足移除條件,即當前佇列如下:
執行nn=n.next;
回收n指向的元素
執行p.next=nn;
這時p之後的佇列又是一個帶頭結點的連結串列。可以繼續while了。