轉載自http://www.dss886.com/2016/08/17/01/
使用handler發送消息時有兩種方式,post(Runnable r)和post(Runnable r, long delayMillis)都是將指定Runnable(包裝成PostMessage)加入到MessageQueue中,然后Looper不斷從MessageQueue中讀取Message進行處理。
然而我在使用的時候就一直有一個疑問,類似Looper這種「輪詢」的工作方式,如果在每次讀取時判斷時間,是無論如何都會有誤差的。但是在測試中發現Delay的誤差並沒有大於我使用System.out.println(System.currentTimeMillis())所產生的誤差,幾乎可以忽略不計,那么Android是怎么做到的呢?
Handler.postDelayed()的調用路徑
一步一步跟一下Handler.postDelayed()的調用路徑:
- Handler.postDelayed(Runnable r, long delayMillis)
- Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
- Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
- Handler.enqueueMessage(queue, msg, uptimeMillis)
- MessageQueue.enqueueMessage(msg, uptimeMillis)
最后發現Handler沒有自己處理Delay,而是交給了MessageQueue處理,我們繼續跟進去看看MessageQueue又做了什么:
-
msg.markInUse();
-
msg.when = when;
-
Message p = mMessages;
-
boolean needWake;
-
if (p == null || when == 0 || when < p.when) {
-
// New head, wake up the event queue if blocked.
-
msg.next = p;
-
mMessages = msg;
-
needWake = mBlocked;
-
} else {
-
...
-
}
MessageQueue中組織Message的結構就是一個簡單的單向鏈表,只保存了鏈表頭部的引用(果然只是個Queue啊)。在enqueueMessage()的時候把應該執行的時間(上面Hanlder調用路徑的第三步延遲已經加上了現有時間,所以叫when)設置到msg里面,並沒有進行處理……WTF?
繼續跟進去看看Looper是怎么讀取MessageQueue的,在loop()方法內:
-
for (;;) {
-
Message msg = queue.next(); // might block
-
if (msg == null) {
-
// No message indicates that the message queue is quitting.
-
return;
-
}
-
...
-
}
原來調用的是MessageQueue.next(),還貼心地注釋了這個方法可能會阻塞,點進去看看:
-
for (;;) {
-
if (nextPollTimeoutMillis != 0) {
-
Binder.flushPendingCommands();
-
}
-
-
nativePollOnce(ptr, nextPollTimeoutMillis);
-
-
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 && msg.target == null) {
-
// Stalled by a barrier. Find the next asynchronous message in the queue.
-
do {
-
prevMsg = msg;
-
msg = msg.next;
-
} while (msg != null && !msg.isAsynchronous());
-
}
-
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;
-
}
-
...
-
}
-
}
可以看到,在這個方法內,如果頭部的這個Message是有延遲而且延遲時間沒到的(now < msg.when),會計算一下時間(保存為變量nextPollTimeoutMillis),然后在循環開始的時候判斷如果這個Message有延遲,就調用nativePollOnce(ptr, nextPollTimeoutMillis)進行阻塞。nativePollOnce()的作用類似與object.wait(),只不過是使用了Native的方法對這個線程精確時間的喚醒。
精確延時的問題到這里就算是基本解決了,不過我又產生了一個新的疑問:如果Message會阻塞MessageQueue的話,那么先postDelay10秒一個Runnable A,消息隊列會一直阻塞,然后我再post一個Runnable B,B豈不是會等A執行完了再執行?正常使用時顯然不是這樣的,那么問題出在哪呢?
再來一步一步順一下Looper、Handler、MessageQueue的調用執行邏輯,重新看到MessageQueue.enqueueMessage()的時候發現,似乎剛才遺漏了什么東西:
-
msg.markInUse();
-
msg.when = when;
-
Message p = mMessages;
-
boolean needWake;
-
if (p == null || when == 0 || when < p.when) {
-
// New head, wake up the event queue if blocked.
-
msg.next = p;
-
mMessages = msg;
-
needWake = mBlocked;
-
} else {
-
...
-
}
-
...
-
// We can assume mPtr != 0 because mQuitting is false.
-
if (needWake) {
-
nativeWake(mPtr);
-
}
這個needWake變量和nativeWake()方法似乎是喚醒線程啊?繼續看看mBlocked是什么:
-
Message next() {
-
for (;;) {
-
...
-
if (msg != null) {
-
...
-
} else {
-
// Got a message.
-
mBlocked = false;
-
...
-
}
-
...
-
}
-
...
-
if (pendingIdleHandlerCount <= 0) {
-
// No idle handlers to run. Loop and wait some more.
-
mBlocked = true;
-
continue;
-
}
-
...
-
}
就是這里了,在next()方法內部,如果有阻塞(沒有消息了或者只有Delay的消息),會把mBlocked這個變量標記為true,在下一個Message進隊時會判斷這個message的位置,如果在隊首就會調用nativeWake()方法喚醒線程!
現在整個調用流程就比較清晰了,以剛剛的問題為例:
postDelay()一個10秒鍾的Runnable A、消息進隊,MessageQueue調用nativePollOnce()阻塞,Looper阻塞;- 緊接着
post()一個Runnable B、消息進隊,判斷現在A時間還沒到、正在阻塞,把B插入消息隊列的頭部(A的前面),然后調用nativeWake()方法喚醒線程; MessageQueue.next()方法被喚醒后,重新開始讀取消息鏈表,第一個消息B無延時,直接返回給Looper;- Looper處理完這個消息再次調用
next()方法,MessageQueue繼續讀取消息鏈表,第二個消息A還沒到時間,計算一下剩余時間(假如還剩9秒)繼續調用nativePollOnce()阻塞; - 直到阻塞時間到或者下一次有Message進隊;
這樣,基本上就能保證Handler.postDelayed()發布的消息能在相對精確的時間被傳遞給Looper進行處理而又不會阻塞隊列了。
