Handler原理分析

卻把清梅嗅發表於2019-04-02

Handler的原理分析這個標題,很多文章都寫過,最近認真將原始碼逐行一字一句研究,特此也簡單總結一遍。

首先是Handler整個Android訊息機制的簡單概括:

Handler原理分析

分三部分對訊息機制的整個流程進行闡述:

  • Handler的建立,包括LooperMessageQueue的建立;
  • Handler傳送訊息,Message是如何進入訊息佇列MessageQueue的(入列);
  • Looper輪詢訊息,Message出列,Handler處理訊息。

一、Handler建立流程分析

1.Handler如何被建立的

// 最簡單的建立方式
public Handler() {
    this(null, false);
}

// ....還有很多種方式,但這些方式最終都執行這個構造方法
public Handler(Callback callback, boolean async) {
  // 1.檢查記憶體洩漏
  if (FIND_POTENTIAL_LEAKS) {
      final Class<? extends Handler> klass = getClass();
      if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
              (klass.getModifiers() & Modifier.STATIC) == 0) {
          Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
              klass.getCanonicalName());
      }
  }

  // 2.通過Looper.myLooper()獲取當前執行緒的Looper物件
  mLooper = Looper.myLooper();
  // 3.如果Looper為空,丟擲異常
  if (mLooper == null) {
      throw new RuntimeException(
          "Can't create handler inside thread " + Thread.currentThread()
                  + " that has not called Looper.prepare()");
  }
  mQueue = mLooper.mQueue;
  mCallback = callback;
  mAsynchronous = async;
}
複製程式碼

首先,如何避免Handler的記憶體洩漏是一個非常常見的面試題,其實Handler的原始碼中已經將答案非常清晰告知給了開發者,即讓Handler的匯出類保證為static的,如果需要,將Context作為弱引用的依賴注入進來。

同時,在Handler建立的同時,會嘗試獲取當前執行緒唯一的Looper物件:

public final class Looper {

  static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

  public static @Nullable Looper myLooper() {
      return sThreadLocal.get();
  }
}
複製程式碼

關於ThreadLocal,我在上一篇文章中已經進行了分析,現在我們知道了ThreadLocal保證了當前執行緒內有且僅有唯一的一個Looper

2.Looper是如何保證執行緒單例的

那就是需要呼叫Looper.prepare()方法:

public final class Looper {

   public static void prepare() {
       prepare(true);
   }

   private static void prepare(boolean quitAllowed) {
       if (sThreadLocal.get() != null) {
           throw new RuntimeException("Only one Looper may be created per thread");
       }
       sThreadLocal.set(new Looper(quitAllowed));
   }

   private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
 }
複製程式碼

這也就說明了,為什麼當前執行緒沒有Looper的例項時,會丟擲一個異常並提示開發者需要呼叫Looper.prepare()方法了。

也正如上述程式碼片段所描述的,如果當前執行緒已經有了Looper的例項,也會丟擲一個異常,提示使用者每個執行緒只能有一個Looperthrow new RuntimeException("Only one Looper may be created per thread");)。

此外,在Looper例項化的同時,也建立了對應的MessageQueue,這也就說明,一個執行緒有且僅有一個Looper,也僅有一個MessageQueue

二、傳送訊息流程分析

1.sendMessage()分析

sendMessage()流程如下:

// 1.傳送即時訊息
public final boolean sendMessage(Message msg)
{
    return sendMessageDelayed(msg, 0);
}

// 2.實際上是發射一個延時為0的Message
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
   if (delayMillis < 0) {
       delayMillis = 0;
   }
   return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

// 3.將訊息和延時的時間進行入列(訊息佇列)
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

// 4.內部實際上還是執行了MessageQueue的enqueueMessage()方法
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
複製程式碼

注意第四步實際上將Handler物件最為target,附著在了Message之上;接下來看MessageQueue類內部是如何對Message進行入列的。

2.MessageQueue訊息入列

boolean enqueueMessage(Message msg, long when) {
    //... 省略部分程式碼
    synchronized (this) {
        msg.markInUse();
        msg.when = when;
        // 獲得連結串列頭的Message
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // 若有以下情景之一,將Message置於連結串列頭
            // 1.頭部Message為空,連結串列為空
            // 2.訊息為即時Message
            // 3.頭部Message的時間戳大於最新Message的時間戳
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 反之,將Message插入到連結串列對應的位置
            Message prev;
            // for迴圈就是找到合適的位置,並將最新的Message插入連結串列
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}
複製程式碼

MessageQueue的資料結構本身是一個單向連結串列

三、接收訊息分析

Handler建立好後,若在此之前呼叫了Looper.prepare()初始化Looper,還需要呼叫Looper.loop()開始該執行緒內的訊息輪詢。

1.Looper.loop()

public static void loop() {
    // ...省略部分程式碼
    // 1. 獲取Looper物件
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    // 2.獲取messageQueue
    final MessageQueue queue = me.mQueue;
    // 3. 輪詢訊息,這裡是一個死迴圈
    for (;;) {
        // 4.從訊息佇列中取出訊息,若訊息佇列為空,則阻塞執行緒
        Message msg = queue.next();
        if (msg == null) {
            return;
        }

        // 5.派發訊息到對應的Handler
        msg.target.dispatchMessage(msg);
        // ...
    }
}
複製程式碼

比較簡單,需要注意的一點是MessageQueue.next()是一個可能會阻塞執行緒的方法,當有訊息時會輪詢處理訊息,但如果訊息佇列中沒有訊息,則會阻塞執行緒。

2.MessageQueue.next()

private native void nativePollOnce(long ptr, int timeoutMillis);


Message next() {

    // ...省略部分程式碼
    int nextPollTimeoutMillis = 0;

    for (;;) {
      // ...

    // native方法
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {

        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
        Message msg = mMessages;

        // 從訊息佇列中取出訊息
        if (msg != null) {
            // 當時間小於message的時間戳時,獲取時間差
            if (now < msg.when) {
                // 該值將會導致在下次迴圈中阻塞對應時間
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
            } else {
                // 取出訊息並返回
                mBlocked = false;
                if (prevMsg != null) {
                    prevMsg.next = msg.next;
                } else {
                    mMessages = msg.next;
                }
                msg.next = null;
                msg.markInUse();
                return msg;
            }
        }
        // ...
   }
}
複製程式碼

注意程式碼片段最上方的native方法——迴圈體內首先呼叫nativePollOnce(ptr, nextPollTimeoutMillis),這是一個native方法,實際作用就是通過Native層的MessageQueue阻塞nextPollTimeoutMillis毫秒的時間:

  • 1.如果nextPollTimeoutMillis=-1,一直阻塞不會超時。
  • 2.如果nextPollTimeoutMillis=0,不會阻塞,立即返回。
  • 3.如果nextPollTimeoutMillis>0,最長阻塞nextPollTimeoutMillis毫秒(超時),如果期間有程式喚醒會立即返回。

搞清楚這一點,其它就都好理解了。

3.最終將訊息傳送給Handler

正如上文所說的,msg.target.dispatchMessage(msg)實際上就是呼叫Handler.dispatchMessage(msg),內部最終也是執行了Handler.handleMessage()回撥:

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        // 如果訊息沒有定義callBack,或者不是通過
        // Handler(Callback)的方式例項化Handler,
        // 最終會走到這裡
        handleMessage(msg);
    }
}
複製程式碼

參考&感謝


關於我

Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github

如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章