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

codelang發表於2019-03-08

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

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

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

已知

  • Main Handler 在 ActivityThread 的時候就 Looper.loop
  • 所有的訊息都是通過 Looper.loop 進行分發

  • Message 訊息佇列對於延遲訊息是如何處理的?

解題步驟氛圍兩步來看:

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

分發訊息

Handler.class

 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 在傳送訊息時都會進入這一步,從這段程式碼中我們捋出幾個重要點:

  • 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 幹了啥:

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;
     }
 }
複製程式碼

繼續捋關鍵點:

  • 時間戳+延遲時間 在這個地方變成了 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小時執行第一個

相關文章