一位熱心羣友在面試時拋了一個問題:java
說下 handler 機制,Looper 經過 MessageQueue 取消息,消息隊列是先進先出模式,那我延遲發兩個消息,第一個消息延遲2個小時,第二個消息延遲1個小時,那麼第二個消息須要等3個小時才能取到嗎?面試
鑑於這個血案,咱們來翻翻案,一探究竟。oop
解題步驟氛圍兩步來看:this
Handler.classspa
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
....
}
複製代碼
Handler 在發送消息時都會進入這一步,從這段代碼中咱們捋出幾個重要點:code
時間戳+延遲時間
(注意,這裏後面須要用上)private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
複製代碼
最終會調用到 enqueueMessage ,這裏給幾個信息:隊列
來看看 MessageQueue.enqueueMessage 幹了啥:get
MessageQueue.class消息隊列
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
...
msg.when = when;
Message p = mMessages;
boolean needWake;
//① 若是進來的消息 when 比當前頭節點 p.when 還小,就想該消息插入到表頭
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
...
Message prev;
for (;;) {
prev = p;
//遍歷鏈表
p = p.next;
//②
//p==null : 只有在遍歷到鏈表尾的時候纔會爲 true
//when < p.when : 上一個消息的延遲大於當前延遲,這個地方就能夠回顧面試的那個問題
//p.when 當作第一個延遲2小時,when 當作目前進來的延遲1小時,這個時候是爲 true
if (p == null || when < p.when) {
break;
}
...
}
//③
msg.next = p;
prev.next = msg;
}
}
複製代碼
繼續捋關鍵點:string
時間戳+延遲時間
在這個地方變成了 when
,而且賦值給了 Message這個地方須要重點講解 ③ 處,這個地方要分類去討論,咱們給出兩個假設和例子:
假設一: p==null 爲 true
p==null
爲 true 的話,也就意味着鏈表遍歷到了鏈尾,而且 when < p.when
一直都爲 false,也就是說進來的消息延遲都是大於當前節點的延遲,這個地方咱們來舉個知足條件例子:
最後的代碼就是意思就是 10s.next=null
、4s.next=10s
,最終鏈表爲:
假設二: when < p.when 爲 true
也就是說,鏈表尚未遍歷到鏈尾發現進來的消息延遲小於當前節點的延遲,而後break了循環體,這個地方也來舉一個知足條件的例子:
遍歷到 4s 的時候,發現 2s < 4s,break,當前 p 節點指向的是節點 4s,則最後代碼的意思就是 2s.next=4s
、1s.next=2s
,最終鏈表爲:
Handler 會根據延遲消息整理鏈表,最終構建出一個時間從小到大的序列
Looper.class
public static void loop() {
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
...
try {
msg.target.dispatchMessage(msg);
}catch()
}
...
}
複製代碼
loop 會一直循環去遍歷 MessageQueue 的消息,拿到 msg 消息後,會將消息 dispatchMessage 發送出去,那麼,me.next() 取消息就顯得尤其重要了,咱們進來看看。
MessageQueue.class
Message next() {
...
int nextPollTimeoutMillis = 0;
for (;;) {
...
synchronized (this) {
// Try to retrieve the next message. Return if found.
//①、獲取當前的時間戳
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
...
if (msg != null) {
//②,若是當前時間戳小於所取延遲消息,則以他們的時間差返回
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//③、延遲時間到了就能夠拿到消息,直接返回了
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
//④、
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
...
nextPollTimeoutMillis = 0;
}
}
複製代碼
詳細解釋下:
②標識:
還記得 msg.when
是由什麼構成的嘛?時間戳+delay
,每次循環都會更新 now
的時間戳,也就是說,當前for循環會一直去執行,直到 now
大於 時間戳+delay
就能夠去取消息了。
④標識:
由於消息的存取都是按時間從小到大排列的,每次取到的消息都是鏈表頭部,這時候鏈頭須要脫離整個鏈表,則設置 next=null。知道最後這個用完的消息去哪了嘛?還記得 obtainMessage 複用消息嗎?
延遲消息的發送是經過循環遍歷,不停的獲取當前時間戳來與 msg.when 比較,直到小於當前時間戳爲止。那經過這段代碼咱們也是能夠發現,經過 Handler.delay 去延遲多少秒是很是不精確的,由於相減會發生誤差
回顧問題,咱們來解答: