Handler.postDelayed()工作原理

轉載:https://blog.csdn.net/qingtiantianqing/article/details/72783952

原文:

使用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()的調用路徑:

  1. Handler.postDelayed(Runnable r, long delayMillis)
  2. Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
  3. Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
  4. Handler.enqueueMessage(queue, msg, uptimeMillis)
  5. MessageQueue.enqueueMessage(msg, uptimeMillis)

最後發現Handler沒有自己處理Delay,而是交給了MessageQueue處理,我們繼續跟進去看看MessageQueue又做了什麼:


 
  1. msg.markInUse();

  2. msg.when = when;

  3. Message p = mMessages;

  4. boolean needWake;

  5. if (p == null || when == 0 || when < p.when) {

  6. // New head, wake up the event queue if blocked.

  7. msg.next = p;

  8. mMessages = msg;

  9. needWake = mBlocked;

  10. } else {

  11. ...

  12. }

MessageQueue中組織Message的結構就是一個簡單的單向鏈表,只保存了鏈表頭部的引用(果然只是個Queue啊)。在enqueueMessage()的時候把應該執行的時間(上面Hanlder調用路徑的第三步延遲已經加上了現有時間,所以叫when)設置到msg裏面,並沒有進行處理……WTF?

繼續跟進去看看Looper是怎麼讀取MessageQueue的,在loop()方法內:


 
  1. for (;;) {

  2. Message msg = queue.next(); // might block

  3. if (msg == null) {

  4. // No message indicates that the message queue is quitting.

  5. return;

  6. }

  7. ...

  8. }

原來調用的是MessageQueue.next(),還貼心地註釋了這個方法可能會阻塞,點進去看看:


 
  1. for (;;) {

  2. if (nextPollTimeoutMillis != 0) {

  3. Binder.flushPendingCommands();

  4. }

  5.  
  6. nativePollOnce(ptr, nextPollTimeoutMillis);

  7.  
  8. synchronized (this) {

  9. // Try to retrieve the next message. Return if found.

  10. final long now = SystemClock.uptimeMillis();

  11. Message prevMsg = null;

  12. Message msg = mMessages;

  13. if (msg != null && msg.target == null) {

  14. // Stalled by a barrier. Find the next asynchronous message in the queue.

  15. do {

  16. prevMsg = msg;

  17. msg = msg.next;

  18. } while (msg != null && !msg.isAsynchronous());

  19. }

  20. if (msg != null) {

  21. if (now < msg.when) {

  22. // Next message is not ready. Set a timeout to wake up when it is ready.

  23. nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

  24. } else {

  25. // Got a message.

  26. mBlocked = false;

  27. if (prevMsg != null) {

  28. prevMsg.next = msg.next;

  29. } else {

  30. mMessages = msg.next;

  31. }

  32. msg.next = null;

  33. if (DEBUG) Log.v(TAG, "Returning message: " + msg);

  34. msg.markInUse();

  35. return msg;

  36. }

  37. } else {

  38. // No more messages.

  39. nextPollTimeoutMillis = -1;

  40. }

  41. ...

  42. }

  43. }

可以看到,在這個方法內,如果頭部的這個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()的時候發現,似乎剛纔遺漏了什麼東西:


 
  1. msg.markInUse();

  2. msg.when = when;

  3. Message p = mMessages;

  4. boolean needWake;

  5. if (p == null || when == 0 || when < p.when) {

  6. // New head, wake up the event queue if blocked.

  7. msg.next = p;

  8. mMessages = msg;

  9. needWake = mBlocked;

  10. } else {

  11. ...

  12. }

  13. ...

  14. // We can assume mPtr != 0 because mQuitting is false.

  15. if (needWake) {

  16. nativeWake(mPtr);

  17. }

這個needWake變量和nativeWake()方法似乎是喚醒線程啊?繼續看看mBlocked是什麼:


 
  1. Message next() {

  2. for (;;) {

  3. ...

  4. if (msg != null) {

  5. ...

  6. } else {

  7. // Got a message.

  8. mBlocked = false;

  9. ...

  10. }

  11. ...

  12. }

  13. ...

  14. if (pendingIdleHandlerCount <= 0) {

  15. // No idle handlers to run. Loop and wait some more.

  16. mBlocked = true;

  17. continue;

  18. }

  19. ...

  20. }

就是這裏了,在next()方法內部,如果有阻塞(沒有消息了或者只有Delay的消息),會把mBlocked這個變量標記爲true,在下一個Message進隊時會判斷這個message的位置,如果在隊首就會調用nativeWake()方法喚醒線程!

現在整個調用流程就比較清晰了,以剛剛的問題爲例:

  1. postDelay()一個10秒鐘的Runnable A、消息進隊,MessageQueue調用nativePollOnce()阻塞,Looper阻塞;
  2. 緊接着post()一個Runnable B、消息進隊,判斷現在A時間還沒到、正在阻塞,把B插入消息隊列的頭部(A的前面),然後調用nativeWake()方法喚醒線程;
  3. MessageQueue.next()方法被喚醒後,重新開始讀取消息鏈表,第一個消息B無延時,直接返回給Looper;
  4. Looper處理完這個消息再次調用next()方法,MessageQueue繼續讀取消息鏈表,第二個消息A還沒到時間,計算一下剩餘時間(假如還剩9秒)繼續調用nativePollOnce()阻塞;
  5. 直到阻塞時間到或者下一次有Message進隊;

這樣,基本上就能保證Handler.postDelayed()發佈的消息能在相對精確的時間被傳遞給Looper進行處理而又不會阻塞隊列了。

 

 

另外,這裏在閱讀原文的基礎上添加一點思考內容:

 

MessageQueue會根據post delay的時間排序放入到鏈表中,鏈表頭的時間小,尾部時間最大。因此能保證時間Delay最長的不會block住時間短的。當每次post message的時候會進入到MessageQueue的next()方法,會根據其delay時間和鏈表頭的比較,如果更短則,放入鏈表頭,並且看時間是否有delay,如果有,則block,等待時間到來喚醒執行,否則將喚醒立即執行。

 

所以handler.postDelay並不是先等待一定的時間再放入到MessageQueue中,而是直接進入MessageQueue,以MessageQueue的時間順序排列和喚醒的方式結合實現的。使用後者的方式,我認爲是集中式的統一管理了所有message,而如果像前者的話,有多少個delay message,則需要起多少個定時器。前者由於有了排序,而且保存的每個message的執行時間,因此只需一個定時器按順序next即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章