回轉壽司你一定吃過!——Android訊息機制(分發)

Taylor發表於2019-02-15

這是“Android訊息機制”系列的第二篇文章,系列文章目錄如下:

  1. 回轉壽司你一定吃過!——Android訊息機制(構造)
  2. 回轉壽司你一定吃過!——Android訊息機制(分發)
  3. 回轉壽司你一定吃過!——Android訊息機制(處理)

訊息機制的故事

壽司陳放在壽司碟上,壽司碟按先後順序被排成佇列送上傳送帶傳送帶被啟動後,壽司挨個呈現到你面前,你有三種享用壽司的方法。

將Android概念帶入後,就變成了Android訊息機制的故事:

  • 壽司碟 ---> 訊息(Message)
  • 佇列 ---> 訊息佇列(MessageQueue)
  • 傳送帶 ---> 訊息泵 (Looper)
  • 壽司 ---> 你關心的資料
  • 享用壽司方法 ---> 處理資料方式

暫未找到 Handler 在此場景中對應的實體。它是一個更抽象的概念,它即可以生產壽司,又把壽司送上傳送帶,還定義了怎麼享用壽司。暫且稱它為訊息處理器吧。

如果打算自己開一家回轉壽司店,下面的問題很關鍵:

  1. 如何生產壽司(如何構造訊息)
  2. 如何分發壽司(如何分發訊息)

關於如何構造訊息可以移步上一篇部落格回轉壽司你一定吃過!——Android訊息機制(構造)。這一篇從原始碼角度分析下“如何分發訊息”。

分發要解決的問題是如何將壽司從廚師運送到消費者。回轉壽司系統是這樣做的:將壽司挨個排好放在傳送帶上,然後讓傳送帶滾動起來。對應的,在Android訊息系統中也有類似的兩個步驟:1. 訊息入隊 2. 訊息泵

(ps: 下文中的 粗斜體字 表示引導原始碼閱讀的內心戲)

1. 訊息入隊

關於入隊需要提兩個基本問題:(1)什麼時候入隊(2)怎麼入隊。第二個問題其實是在問“訊息佇列的資料結構是什麼?”。特定資料結構對應特定插入方法。 對於訊息佇列一無所知的我完全沒有了頭緒,這原始碼該從哪裡開始讀起?沒有思路的時候我們還可以YY(YY是人類特有的強大技能)。憑藉著對資料結構殘存的記憶,我隱約覺得“入隊”應該是佇列提供的基本操作,那就先從MessageQueue開始讀吧~

/**
 * Low-level class holding the list of messages to be dispatched by a
 * {@link Looper}.  “Messages are not added directly to a MessageQueue,
 * but rather through {@link Handler} objects associated with the Looper.”
 * <p>
 * <p>You can retrieve the MessageQueue for the current thread with
 * {@link Looper#myQueue() Looper.myQueue()}.
 */
public final class MessageQueue
{
    ...
}
複製程式碼

註釋提供了關鍵提示,它說“訊息不是直接加到訊息佇列中的,而是通過Handler物件”。不急著去看Handler,先找一下MessageQueue是否有“入隊操作”。

//省略了一些非關鍵程式碼    
boolean enqueueMessage(Message msg,
                           long when)
    {
        ...
        synchronized (this)
        {
            ...
            msg.markInUse();
            msg.when = when;
            //p指向訊息佇列頭結點
            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
            {
                ...
                //從訊息佇列隊頭開始尋找合適的位置將訊息插入
                Message prev;
                for (; ; )
                {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when)
                    {
                        break;
                    }
                    if (needWake && p.isAsynchronous())
                    {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
        }
        ...
        return true;
    }
複製程式碼
  • 不出所料,果然有一個入隊函式,訊息佇列的資料結構和訊息池一模一樣(訊息池的介紹可以點選這裡),都是連結串列。
  • 看到這裡第二個問題基本解決了:訊息是通過連結串列的插入操作進入訊息佇列的。讓我們思考的再深入一點:新訊息插入到連結串列的什麼位置? 可以看到原始碼中有一個大大的if-else,判斷條件是傳入的引數when,沿著呼叫鏈往上搜尋,在Handler中會發現如下函式:
    /**
     * “Enqueue a message into the message queue after all pending messages
     * before the absolute time (in milliseconds) <var>uptimeMillis</var>.”
     * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
     * Time spent in deep sleep will add an additional delay to execution.
     * You will receive it in {@link #handleMessage}, in the thread attached
     * to this handler.
     * 
     * @param uptimeMillis “The absolute time at which the message should be
     *         delivered, using the
     *         {@link android.os.SystemClock#uptimeMillis} time-base.”
     *         ...
     */
    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);
    }
複製程式碼
  • 註釋中帶引號的那句話很關鍵:“將訊息插入到訊息佇列中,並且排在所有uptimeMillis之前產生的訊息後面”。uptimeMillis表示訊息被髮送的時間。這麼看來,訊息是按時間先後順序排列的,最舊的訊息在隊頭,最新的訊息在隊尾。那訊息入隊就分兩種情況:1. 尾插入 2.中間插入。其中尾插入表示最新的訊息插入隊尾。回頭再看一遍MessageQueue.enqueueMessage(),那個大大的if-else就實現了這兩種情況。
  • Handler.sendMessageAtTime()沿著呼叫鏈繼續往上搜尋,就會找到下面這個熟悉的方法:
    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }
複製程式碼
  • 這不就是我們用來發訊息的Handler.sendMessage()嗎!至此,讓我們總結一下“訊息入隊”:Handler傳送訊息就是將訊息按時間順序插入到訊息佇列,訊息佇列是連結串列結構,鏈頭是最舊的訊息,鏈尾是最新的訊息

2. 訊息泵

壽司已經按時間順序排列好了,是時候按下按鈕啟動傳送帶讓壽司迴圈起來了。對於Android訊息機制來說,讓訊息迴圈起來就表現為不斷從訊息佇列中拿訊息。MessageQueue中有入隊操作,必然有出隊操作

//省略大量非關鍵程式碼
   Message next() {
        ...
        for (;;) {
            ...
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                //msg指向訊息佇列隊頭
                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;
                }
                ...
            }
        }
  }
複製程式碼
  • 這個函式很長,省略了一些和主題不相關的細節,比如:佇列空閒等待,非同步訊息。去掉了這些特殊情況後,出隊操作就是取訊息佇列的頭(佇列頭是最舊的訊息,佇列尾是最新的訊息),這符合佇列先進先出的特性,越早的訊息越先被分發。
  • 必然有一個迴圈會不停的呼叫 MessageQueue.next(),從訊息佇列中不斷的取訊息進行分發,經過一頓搜尋,果然在 Looper中找到了:
    public static void loop()
    {
        //獲得當前執行緒的訊息泵
        final Looper me = myLooper();
        if (me == null)
        {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        //獲得當前執行緒的訊息佇列
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        //nandian
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        //取訊息的無限迴圈
        for (; ; )
        {
            //從隊頭取出訊息(可能阻塞)
            Message msg = queue.next(); // might block
            //沒有訊息則退出迴圈
            if (msg == null)
            {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            Printer logging = me.mLogging;
            if (logging != null)
            {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                                        msg.callback + ": " + msg.what);
            }

            //分發訊息
            msg.target.dispatchMessage(msg);

            if (logging != null)
            {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn‘t corrupted.
            //nandian
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent)
            {
                Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass()
                                                                                                                                                                        .getName() + " " + msg.callback + " what=" + msg.what);
            }

            //回收訊息
            msg.recycleUnchecked();
        }
複製程式碼
  • 這個函式是Android訊息機制中構造並分發訊息的終點,處理訊息的起點。Looper通過無限迴圈從訊息佇列中取出最舊的訊息,並分發給訊息對應的訊息處理器,最後回收訊息。至此,上一篇文章回轉壽司你一定吃過!——Android訊息機制(構造)中留下的疑問就解決了:訊息是在被分發後立馬回收的。
  • 當訊息佇列中沒有訊息時,queue.next()就會阻塞當前執行緒。再也不用擔心Looper.loop()中的額無限迴圈會消耗CPU資源了。
  • Looper.loop()什麼時候會被呼叫? 我們都知道主執行緒自帶Looper,雖然這是一個很好的切入點,但其中牽涉到太多和主題無關的內容。所以換一個更純粹的切入點,HandlerThread
/**
 * Handy class for starting a new thread that has a looper. The looper can then be 
 * used to create handler classes. Note that start() must still be called.
 */
public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private @Nullable Handler mHandler;

    @Override
    public void run() {
        mTid = Process.myTid();
        //1.準備Looper
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        //2. Looper開始迴圈
        Looper.loop();
        mTid = -1;
    }
}
複製程式碼
  • 註釋又一次給了我們很多提示:“該類用於建立帶有Looper的執行緒”。 難道並不是所有的執行緒都帶有Looper?(想想也是:執行緒是一個Java概念,Looper是一個Android概念) 。所以我們需要線上程啟動的時候特意做些什麼才能得到帶有Looper的執行緒。在Thread.run()中看到了兩個關鍵方法,其中Looper.loop()已經分析過了,在它之前還有一個Looper.prepare(),點進去看看:
/**
  * “Class used to run a message loop for a thread.  Threads by default do
  * not have a message loop associated with them; to create one, call
  * {@link #prepare} in the thread that is to run the loop, and then
  * {@link #loop} to have it process messages until the loop is stopped.”
  */
public final class Looper {
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    final MessageQueue mQueue;

     /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    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));
    }
}
複製程式碼
  • 一臉茫然的時候就看註釋,帶引號的註釋揭露了關鍵真相:Looper用於為執行緒建立訊息迴圈系統,預設情況下執行緒沒有和它相關聯的訊息迴圈系統。可以通過線上程中呼叫Looper.prepare()來啟動一個訊息迴圈系統,接著呼叫Looper.prepare()來迴圈處理訊息
  • prepare()中,新建了Looper例項,並且設定給ThreadLocal物件,這個類用於保證執行緒物件和自定義型別物件一對一的關係。這個一個很大的主題,就不展開了。當下只要知道Looper通過它將自己的例項和某一個執行緒繫結,即一個執行緒只有一個Looper物件。所以Android訊息系統的層級結構是這樣的:1個Thread 對應 1個Looper,1個Looper有1個MessageQueue,1個MessageQueue有若干Message

總結

Android訊息機制中的“分發訊息”部分講完了,總結一下:傳送訊息時,訊息按時間先後順序插入到訊息佇列中,Looper遍歷訊息佇列取出訊息分發給對應的Handler處理

故事還沒有結束,下一篇會繼續講解處理訊息

相關文章