一個 Handler 面試題引起的血案!!!

一位熱心羣友在面試時拋了一個問題:java

說下 handler 機制,Looper 經過 MessageQueue 取消息,消息隊列是先進先出模式,那我延遲發兩個消息,第一個消息延遲2個小時,第二個消息延遲1個小時,那麼第二個消息須要等3個小時才能取到嗎?面試

鑑於這個血案,咱們來翻翻案,一探究竟。oop

已知

  • Main Handler 在 ActivityThread 的時候就 Looper.loop
  • 全部的消息都是經過 Looper.loop 進行分發

  • Message 消息隊列對於延遲消息是如何處理的?

解題步驟氛圍兩步來看:this

  • 分發消息 sendMessageDelayed
  • 接收消息 dispatchMessage

分發消息

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

  • delay 設置的延遲時間低於0時默認爲0
  • uptimeMillis 爲當前 時間戳+延遲時間 (注意,這裏後面須要用上)
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
}
複製代碼

最終會調用到 enqueueMessage ,這裏給幾個信息:隊列

  • msg.target 指當前建立的 Handler
  • mAsynchronous 默認爲 false
  • 最終調用 MessageQueue.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,也就是說進來的消息延遲都是大於當前節點的延遲,這個地方咱們來舉個知足條件例子:

  • 原消息鏈:0s -> 0s -> 1s -> 4s
  • 進來延遲消息爲 10s

最後的代碼就是意思就是 10s.next=null 、4s.next=10s  ,最終鏈表爲:

  • 0s -> 0s -> 1s -> 4s -> 10s

假設二: when < p.when 爲 true

也就是說,鏈表尚未遍歷到鏈尾發現進來的消息延遲小於當前節點的延遲,而後break了循環體,這個地方也來舉一個知足條件的例子:

  • 原消息鏈:0s -> 0s -> 1s -> 4s
  • 進來延遲消息爲 2s

遍歷到 4s 的時候,發現 2s < 4s,break,當前 p 節點指向的是節點 4s,則最後代碼的意思就是 2s.next=4s 、1s.next=2s ,最終鏈表爲:

  • 0s -> 0s -> 1s -> 2s -> 4s

總結

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 去延遲多少秒是很是不精確的,由於相減會發生誤差

回顧問題,咱們來解答:

  • MessageQueue 的實現不是隊列,不要被名稱迷惑,他是一個鏈表
  • 每次發送消息都會按照 delay 從小到大進行重排
  • 全部的 delay 消息都是並行的,不是串行的
  • 第一個延遲2個小時,第二個延遲1小時,會優先執行第二個,再過1小時執行第一個
相關文章
相關標籤/搜索