反思 Android 消息機制的設計與實現

語言: CN / TW / HK

theme: qklhk-chocolate

上篇文章介紹了 Android 中的 Binder 機制。Binder 在 Android 系統中佔有着舉足輕重的地位,它是 Android 系統中跨進程通信最重要的方式。而另外一個重要的且能與Binder相提並論的角色便是本文要分析的 Handler。Binder 支撐起了 Android 系統進程間的通信,而 Handler 支撐起的則是進程內線程間的通信。同時,Android 應用程序的運行皆依靠 Handler 的消息機制驅動,這其中就包括觸摸事件的分發、View的繪製流程、屏幕的刷新機制以及Activity的生命週期等等。

關於 Handler 其實早在幾年前筆者就寫過一篇《追根溯源—— 探究Handler的實現原理》的文章。 但是鑑於當時對於 Handler 的理解並不那麼深刻,所以這篇文章的內容與網上大多數寫 Handler 的文章一樣僅僅是源碼分析,對 Android 消息機制沒有一個深刻的理解和認識。當然,並不是説這樣的文章不好,對於初學者來説更適合閲讀這樣的文章的。所以,如果你對於 Handler 還沒有太熟悉的話,不妨先讀一讀。

如今,作為一個已有多年 Android 開發經驗的從業者,在閲讀了大量的 framework 源碼之後,對於 Android 的消息機制有了一些更加深刻的認識,這是要寫這篇文章的原因。

一、從“生產者-消費者”模型説起

關注筆者比較久的同學可能看過我之前寫過的一篇文章 《深入理解Java線程的等待與喚醒機制》。在這篇文章中為了分析 synchronized 鎖的等待與喚醒機制,舉了一個 “生產者-消費者” 問題的例子。

“生產者-消費者” 問題又稱有限緩衝問題(Bounded-buffer problem),是一個多線程同步問題的經典案例。該問題描述了共享固定大小緩衝區的兩個線程——即所謂的“生產者”和“消費者”——在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。與此同時,消費者會在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據。 ”生產者-消費者“模型圖如下:

生產者-消費者

可以看得出來,圖中的 "生產者"和"消費者"處於兩個不同的線程,但是他們共用了同一個隊列。生產者在完成數據的生產後會通過 notify 方法喚醒消費者線程,當隊列滿的時候,生產者線程會調用 wait 方法來阻塞自身。同時,消費者線程在被喚醒後則會從隊列中取出數據,並通過 notify 方法喚醒生產者線程繼續生產數據。當隊列中的數據被取空的時候,消費者線程同樣會調用 wait 方法阻塞自身。

關於”生產者-消費者“模型的代碼實現就不在這裏重複貼出了,大家可以到《深入理解Java線程的等待與喚醒機制》這篇文章中閲讀第一章的內容,這些內容對於閲讀本文會有一定的幫助。

”生產者-消費者“模型的案例在平時的開發中是很常見的。例如 Rxjava 的流控制就是典型的”生產者-消費者“模型,除此之外還有線程池的內部實現,以及 Android 系統中輸入事件的採集與派發都是基於”生產者-消費者“模型設計的。

上文提到”生產者-消費者“模型解決的是多個線程共享內存的有限緩衝問題。但其實它還解決了另外一個重要問題,即實現了線程間的通信。在生產與消費的過程中由於共用了同一個緩衝隊列,”生產者“產生的數據從生產者線程傳遞給了消費者線程,對緩衝隊列內的數據而言就實現了線程的切換。

瞭解了“生產者-消費者”模型之後對於 Android 理解消息機制的設計思想會有很大的幫助。

二、設計消息機制的框架

現在讓我們回到 Android 來想一下 Android 中的場景。在文章開頭已經提到 Android 應用中觸摸事件的分發、View的繪製、屏幕的刷新以及 Activity 的生命週期等都是基於消息實現的。這意味着在 Android 應用中隨時都在產生大量的 Message,同時也有大量的 Message 被消費掉。

另外我們都知道在 Android 系統中,UI更新只能在主線程中進行。因此,為了更流暢的頁面渲染,所有的耗時操作包括網絡請求、文件讀寫、資源文件的解析等都應該放到子線程中。在這一場景下,線程間的通信就顯得尤為重要。因為我們需要在網絡請求、文件讀寫或者資源解析後將得到的數據交給主線程去進行頁面渲染。

那在這樣的背景下如果讓我們作為 Android 系統的設計者,會如何設計並實現 Android 的消息機制,讓其即滿足具有緩衝功能又能實現線程切換的能力呢?

有了第一章的知識後我想你一定會很自然的想到使用”生產者-消費者“模型來實現。因為這一模型既能解決數據緩衝問題,又實現了數據在線程間的切換。

沒錯,Android系統的設計者也是這麼想的,於是便誕生 Handler 這一傑作!接下來讓我們跟隨消息機制設計者們的思維來看一下如何實現這一功能。

1.設計消息緩衝區--MessageQueue

由於系統中無時無刻都在產生消息,因此我們首先需要有一個消息緩衝區,用來存放各個生產者線程所產生的消息,我們將這個緩衝區命名為 MessageQueue。作為一個消息隊列,其內部需要有一個存放消息的容器。同時需要對外提供插入消息和取出消息的接口,將這兩個接口方法分別命名為 enqueueMessage(Message msg)next()。MessageQueue 偽代碼實現如下:

```java public class MessageQueue { // 消息容器,暫且使用 LinkedList。 private LinkedList list = new LinkedList<>();

public synchronized void enqueueMessage(Message msg) {
    list.add(msg);
}

public synchronized Message next() {

     while (list.isEmpty()) {
        try {
            // 如果容器為空,則阻塞消費者線程
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    return list.removeFirst()  
 }

} }

```

在 MessageQueue 中維護了一個集合,當插入消息時將消息存入這個集合,取出消息時,將消息從集合中移除並返回。

有了消息隊列之後,還需要有”生產者“與”消費者“這兩個角色,生產者負責向 MessageQueue 中插入消息,而消費者負責從 MessageQueue 中取出消息並進行消費。

2.設計消息機制的”生產者“--Handler

系統各處產生的消息需要存入到消息隊列中,等待消費者取出消息並將其消費。因此我們可以設計一個包裝類來實現消息插入到消息隊列,將這個包裝類命名為 Handler。既然作為消息隊列的包裝, Handler 肯定要持有 MessageQueue,以便於向消息隊列中插入消息。同時還需要對外提供插入消息的API,可以將這個插入消息的API命名為 sendMessage(Message msg),在這個方法內實現把 Message 插入到 MessageQueue 的邏輯。

另外,作為Handler的使用方,在通過 Handler 將消息插入到 MessageQueue 後,肯定迫切的需要知道消息何時被消費者處理。因此,還需要在 Handler 中添加一個處理消息的回調方法,以便於使用方重寫該方法,並完成需要的邏輯。我們將這個方法命名為dispatchMessage。至此,”生產者“的設計就完成了。Handler 的偽代碼實現如下:

```java public class Handler {

private MessageQueue mQueue;

// 這裏的 MessageQueue 應當與消費者的 MessageQueue 是同一個 public Handler(MessageQueue queue){ mQueue = queue; }

// 處理消息的回調 public void dispatchMessage(Message msg){

}

// 向消息隊列中插入消息 public void sendMessage(Message msg) { // Message 中需要持有Handler,以便回調 dispatchMessage 方法。 msg.target = this; mQueue.enqueueMessage(msg); }

// ... 另外還可以實現多個 sendMessage 的重載方法,以適用不同的需求。 } ```

3.設計消息機制的信使-- Message

Message 應該充當的是信使的作用,即 Message 需要攜帶調用方賦予的數據。而這一數據類型並不確定,因此我們可以將它聲明為 Object 類型。另外,在消息被消費者處理的時候需要通知調用方消息被處理了。因此可以讓 Message 持有一個 Handler,以便在消息被處理後回調給Handler。我們將這個 Handler 命名為 target。於是 Message 的偽代碼就可以有如下實現:

java public class Message{ // 攜帶的消息 Object obj; // 持有 Handler Handler target; // ... }

4.設計消息機制的”消費者“--Looper

在完成了消息隊列、生產者、以及消息信使的設計之後,我們還需要實現消費者這一角色。作為消費者,它的職責就是從 MessageQueue 中取出 Message ,並將其消費。值得注意的是這個 MessageQueue 必須是與生產者所共用的。這裏我們將”消費者“這一角色命名為 Looper。Looper作為”消費者“,其職責就是需要不斷的從 MessageQueue 中取出消息並進行消費。那麼我們就將這個取消息的方法命名為 loop(),因為需要不斷的從 MessageQueue 中取出消息,所以這個方法應該被設計成一個死循環,沒有消息的時候就阻塞執行。因此 Looper 的偽代碼實現如下:

```java public class Looper {

// 在 Looper 中實例化消息隊列,並提供給Handler,實現生產者與消費者共享
MessageQueue mQueu = new MessageQueue();

// 從消息隊列中取出消息並消費
public static void loop(){
    for(;;) {
        // 靜態方法,需要獲取Looper的實例
        Message msg = myLooper().mQueue.next();
        if(msg == null) {
            return;
        }
        // 回調到 Handler
        msg.target.dispatchMessage(mgs);
    }
}

// 這裏假設通過myLooper方法拿到looper的實例
public Looper myLooper(){
    return new Looper();
}

} ```

通過”生產者-消費者“模型,我們可以很輕鬆的寫出 Android 消息機制的大體框架.而接下來我們要思考的是在這個實現過程中會面臨什麼樣的問題,以及該如何去解決。

三、完善消息機制的實現邏輯

上一章中,我們搭建起了消息機制的大體框架,下一步就是要實現具體的邏輯了。仔細想一想會發現我們面臨着不少的問題,我列舉出了以下幾個,不妨來思考思考。

  • 線程切換是消息機制的一個重要功能,應該如何實現?
  • 一個線程可以有多個 Looper 實例嗎?如果不可以,那應該如何保證線程級別的 Looper 單例?
  • APP 在運行時會產生大量的 Message,每次都通過 "new" 關鍵字實例化 Message 可行嗎 ?
  • 系統發送的某些消息具有較高的優先級,如何才能保證其優先執行?

1.實現線程切換

消息機制一個很重要的需求,即在子線程中獲取到的數據需要發送給主線程進行頁面渲染。這個讓數據從子線程切換到主線程的功能該如何實現呢?

這其實是一個很簡單的問題,只需要在主線程中實例化 Looper,同時在實例化 Handler 的時候在 Handler 構造方法中傳入 Looper 持有的 MessageQueue 即可。這樣 Looper 和 Handler 共享了同一個 MessageQueue。不管 Handler 在哪個線程發送消息,最終 Looper 都會在主線程中取出消息並執行。看一下代碼的實現:

```java public class MyActivityThread { public void main(String[] args){

    // 實例化一個 Looper
    Looper looper = Looper.myLooper();

    // 實例化 Handler,並傳入 Looper 持有的 MessageQueue
    Handler handler = new Handler(looper.mQueue) {
        @Override
        public void dispatchMessage(Message msg){
            // 在主線程中得到了 Message
        }
    };

    // 開啟一個子線程
    new Thread() {
        @Override
        public void run(){
            // 在子線程中通過 Handler 發出一個消息
            handler.sendMessage(new Message());
        }

    }

    // 調用loop,開啟循環不斷的從MessageQueue中取出消息,沒有消息就會被阻塞。
    // 通過這樣的方式,導致 main 方法不會被執行完而退出程序,Android 系統源碼也是
    // 這樣實現的,這也是為什麼 Android 中的 APP 不會像java程序一樣,執行完邏輯就結束掉。
    looper.loop();
}

} ```

當然,這裏舉的是子線程與主線程的例子,對於子線程與子線程的切換是與之類似的。

2.實現線程級別的 Looper 單例

先來看這個問題,一個線程可以有多個 Looper 實例嗎?一個線程中有多個 Looper 站在邏輯的角度來看顯然是沒什麼問題的。但是如果站在 Android 系統的角度來考慮,一個線程有多個Looper實例顯然有很大的問題!因為 Looper 的 loop() 方法是一個阻塞方法。如果在一個線程中實例化了多個 Looper,並且都調用了它的 loop 方法,那麼一定只有第一個調用 loop 方法的 Looper 實例會運行,其他的 Looper 會被阻塞永遠也執行不了。

因此,作為消息機制的設計者,我們應該保證單個線程只能實例化一個 Looper。而不能寄託於使用者,要求他們在使用 Looper 的時候只實例化一次。

那麼這個時候再來回顧一下上一章我們對 Looper 的設計,似乎是有很大缺陷的。因為此時的Looper可以在主線程中通過 myLooper 方法實例化出任意多個 Looper 對象。顯然這是不符合我們的需求的。有的同學説可以將 Looper 設計成單例,這樣就不會被實例化出多個 Looper 了。但這樣顯然也是不符合需求的,我們需要的是在同一個線程裏邊只能有一個 Looper 實例,但多個線程可以有多個 Looper 實例。也就是説這個 Looper 應該是一個線程級別的單例。那應該怎麼實現呢?

説到這裏,java基礎掌握比較好的同學應該已經想到了,可以使用 ThreadLocal來實現!

ThreadLocal提供了線程級別的數據存儲能力。即在A線程中使用 ThreadLocal 存儲了一個數據,那麼這個數據只對A線程可見,只有在A線程中才能取出,其他線程無法取到。關於 ThreadLocal 瞭解這麼多就足夠了,這裏不再贅述 ThreadLocal 的實現原理,如果你對 ThreadLocal 比較感興趣可以參考我之前寫的一篇文章《Java併發系列番外篇:ThreadLocal原理其實很簡單》

有了以上理論的支持,我們就可以重構 Looper 的實現邏輯了。修改後的 Looper 代碼如下:

```java public class Looper {

// 使用 ThreadLocal 存儲Looper
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

MessageQueue mQueu;

// 私有化構造方法,避免單個線程中被多次實例化
private Looper() {
    // 實例化 MessageQueue
    mQueue = new MessageQueue();
}

// 添加一個 prepare 方法來實例化 Looper,並將其存儲到 TheadLocal
private static void prepare() {
    // 需要保證Looper是線程唯一的
    if (sThreadLocal.get() != null) {
        // 走到這裏説明該線程已經實例化過 Looper 了,拋出異常終止程序執行
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    // 將 Looper 實例存入到 ThreadLocal
    sThreadLocal.set(new Looper());
}

// 從 ThreadLocal 中取出 Looper 實例
public static Looper myLooper() {
    return sThreadLocal.get();
}

// 從消息隊列中取出消息並消費
public static void loop(){
    for(;;) {
        Message msg = myLooper().mQueue.next();
        if(msg == null) {
            return;
        }
        msg.target.dispatchMessage(mgs);
    }
}

} ```

通過以上代碼,我們實現了一個線程級別的單例,保證了每個線程只能創建一個 Looper,多次創建就會導致程序崩潰。

3.Message 對象池

Android APP 在運行的時候會有大量的 Message 由系統插入到 MessageQueue 中,前面已經提到過的 View 的繪製過程、事件分發過程、Activity 啟動過程等等都會向 MessageQueue 插入消息。這就意味着會有大量的 Message 被實例化。如果每次用到 Message 的時候都通過 "new" 關鍵字來實例化實現 Message 對象,那麼肯定會導致嚴重的內存抖動問題。

因此,為了避免 Message 的頻繁實例化,我們可以對獲取 Message 對象的過程做一些優化。通常避免頻繁的創建對象的解決方案都是使用對象池。也就是維護一個 Message 對象池,在用完之將後將 Message 的數據進行重置,並將其放入到對象池中,等待下次複用。這樣就避免了頻繁實例化 Message 可能導致的內存抖動問題。主要是瞭解一下池化思想,這裏就直接 copy 系統的源碼了,系統源碼中 Message 是一個擁有鏈表結構的類。

```java public final class Message implements Parcelable {

public Object obj;

Message next;

// 對象池中有空閒對象直接使用,沒有則實例化Message
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

// 回收 Message,並加入到對象池
public void recycle() {
    if (isInUse()) {
        if (gCheckRecycle) {
            throw new IllegalStateException("This message cannot be recycled because it "
                    + "is still in use.");
        }
        return;
    }
    recycleUnchecked();
}

void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

public Message() {
}

} ```

可以看到 Message 實現對象池的兩個核心方法就是 obtain() 與 recycle(),obtain負責從對象池中取出 Message,如果對象池沒有空閒的 Message,則直接實例化 Message,而 recycle() 則是回收用完的Message,並將其插入到複用鏈表中。這裏所謂的對象池其實就是一個空閒的 Message 鏈表。

4. 無界隊列 MessageQueue

在上一小節中我們已經知道 Message 其實是一個擁有鏈表結構的類。因此 MessageQueue 中的容器其實並非像第二章第1小節中寫的那樣是一個 LinkedList,而是一個 Message 鏈表。

通常來説”生產者-消費者“模型中的緩衝隊列是有特定的容量的,在緩衝隊列填滿的時候就會阻塞生產者繼續添加數據。因此,一個標準的”生產者-消費者“模型必然要考慮背壓策略,就比如大家所熟知的 RxJava 由於內部使用的是有界隊列,因此當隊列的容量不足時就會拋出 MissingBackpressureException。而 Rxjava 也給出了多個背壓策略,例如丟棄事件、擴容、或者直接拋出異常。與之類似的是線程池的實現,區別是線程池內不叫背壓策略,而是叫拒絕策略

但是作為接收系統消息的 MessageQueue 如果被設計成有界隊列合適嗎?顯然是不合適的,因為系統發送的消息多是一些中要的消息,任何事件的丟失都可能會導致嚴重的系統 bug。所以作為消息機制設計者一定會把 MessageQueue 設計成一個無界隊列。這樣插入消息永遠不會被阻塞,也不用考慮所謂的背壓策略了。這是消息機制與標準的 ”生產者-消費者“ 模型的區別之一。

關於 MessageQueue 插入消息與取出消息的實現,前面我們只是簡單寫了偽代碼,而且是使用集合實現的。由於我們已經知道系統源碼中的 Message 是一個鏈表結構的類。因此,我們可以使用鏈表的結構來實現插消息和取消息。

因為這兩個方法的實現還是比較複雜的,因此現在我們跳出設計者的身份,跟隨 Android 系統源碼來解讀這兩個方法的實現。

(1)插入消息的實現

MessageQueue 插入消息的邏輯是在 enqueueMessage 方法中實現的, 簡化後的源碼如下:

```java boolean enqueueMessage(Message msg, long when) {

synchronized (this) {

    msg.when = when;

    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;
    }

    // We can assume mPtr != 0 because mQuitting is false.
    if (needWake) {
        // 喚醒線程
        nativeWake(mPtr);
    }
}
return true;

} ``` enqueueMessage 的邏輯其實並不難理解,就是把 Message 插入到鏈表中去,同時插入鏈表的位置是根據消息 delay 的時間決定的,delay 的時間越長,插入隊列時就越靠後,即越晚執行。最後根據是否要喚醒線程來調用 nativeWake, 這個方法是在 native 層實現的。可以把他理解為"生產者-消費者"模型中 通過 notify 喚醒線程的操作。

(2)取出消息的實現

取出消息是通過 MessageQueue 的 next 方法實現的,簡化後的 next 方法的源碼如下:

```java Message next() {

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {

    // 阻塞線程
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {

        // ... 省略異步消息的處理

        if (msg != null) {

            if (now < msg.when) { // 根據delay的時間判斷該Message是否可以執行
                // 未到執行時間則走到下一次循環調用nativePollOnce阻塞該方法
                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;
            }
        } else {
            // 消息隊列為空,阻塞時間設置於為-1,表示一直阻塞
            nextPollTimeoutMillis = -1;
        }
    }

    // 省略 IdelHandler 的處理邏輯
}

} ``` 這裏暫時忽略 next 方法的異步消息處理邏輯以及 IdleHandler 的處理邏輯。簡化後的代碼並也容易理解。首先是一個用 for 實現的死循環,循環中先調用 nativePollOnce 對線程進行阻塞,這個方法也是一個 native 方法,可以將它理解為”生產者-消費者“模型中阻塞線程的 wait 方法。這個方法的第二個參數表示阻塞的時間,如果是正數,則表示阻塞這個值的毫秒時長,如果是0表示不阻塞,如果小於0,則會一直阻塞。

接下來的邏輯則是判斷消息是不是到了該執行的時間了,如果沒到,則繼續 for 循環執行 nativePollOnce 來阻塞方法,如果消息到了執行的時間就將消息從鏈表中取出並返回給 Looper,交給 Looper 對消息進行消費。

5.Message 的優先級

在標準的”生產者-消費者“模型中消息是沒有優先級之分的。即按照標準的隊列執行先進先出的邏輯。但是在 Android 消息機制中是需要對消息進行優先級劃分的,普通消息應該將優先執行的權利讓給那些會影響程序性能的消息,比如 View 繪製的消息、屏幕刷新的消息以及Activity啟動的消息等。這是消息機制與標準的”生產者-消費者“模型的又一重要區別。

就我的理解而言,消息機制中的消息一共被劃分了四個優先級。優先級由高到低分別是異步消息普通消息IDleHandler 以及延遲消息

(1)異步消息

在 Message 的源碼中為開發者提供了一個 setAsynchronous 的方法,這個方法是對外開放的。通過這個方法會為消息設置一個異步標記。使用代碼如下:

java Message message = Message.obtain(); // 設置異步消息 message.setAsynchronous(true); 異步消息擁有最高的執行優先級,但是僅僅將其設置為異步消息並沒有什麼作用。它需要配合同步屏障消息來執行。什麼是同步屏障消息呢?其實就是一個 Message.target 為 null 的消息。它的實現邏輯在 MessageQueue 的 postSyncBarrier 方法中。源碼如下:

```java // MessageQueue

@UnsupportedAppUsage // 不支持APP調用 @TestApi public int postSyncBarrier() { return postSyncBarrier(SystemClock.uptimeMillis()); }

private int postSyncBarrier(long when) { // Enqueue a new sync barrier token. // We don't need to wake the queue because the purpose of a barrier is to stall it. synchronized (this) { final int token = mNextBarrierToken++; final Message msg = Message.obtain(); msg.markInUse(); msg.when = when; msg.arg1 = token;

    Message prev = null;
    Message p = mMessages;
    if (when != 0) {
        while (p != null && p.when <= when) {
            prev = p;
            p = p.next;
        }
    }
    if (prev != null) { // invariant: p == prev.next
        msg.next = p;
        prev.next = msg;
    } else {
        msg.next = p;
        mMessages = msg;
    }
    return token;
}

} `` 可以看到 postSyncBarrier 方法被@UnsupportedAppUsage` 註解所修飾,意味着這個方法對開發者是不可見的。而 postSyncBarrier 中的邏輯其實就是向鏈表的頭部插入了一條 Message。而這個 Message 與普通消息不同的是,它的 target 並沒有被賦值。在上一節中分析 MessageQueue 的 next 方法時我們忽略了異步消息的處理邏輯。現在來具體看一下這段代碼的實現:

```java Message next() {

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {

    // 阻塞線程
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
        // 消息隊列頭
        Message msg = mMessages;
        // 如果 msg 不為 null,並且 msg.target 為 null,則執行if中的邏輯
        if (msg != null && msg.target == null) {
            // 走到這裏説明讀取到了同步屏障消息
            // 通過 do...while 循環遍歷message鏈表,找到異步消息
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous());
        }

        // ... 省略取消息邏輯
    }

    // 省略 IdelHandler 的處理邏輯

}

} ``` 可以看到這裏首先判斷如果 msg 不為 null,並且 msg.target 為 null,則執行if中的邏輯,而if中則通過 do...while 循環來遍歷 message 鏈表,直到找到異步消息才會終止do...while。然後取出異步消息執行下邊的消息處理邏輯。不難看出,當遇到同步屏障消息之後就會阻塞普通消息的執行。然後遍歷 Message 鏈表找到被標記為異步的消息優先執行。

但是有個問題,同步屏障消息是在什麼時候被插件 MessageQueue 的呢?答案是在ViewRootImpl 中當計劃開始遍歷 View 樹的時候。看下 ViewRootImpl 的 requestLayout 方法,其源碼如下:

```java // ViewRootImpl.java

@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { // 檢查是否是在主線程中,如果不是主線程則直接拋出異常 checkThread(); // mLayoutRequested標記設置為true,在同一個Vsync週期內,執行多次requestLayout的流程 mLayoutRequested = true; // 計劃遍歷View樹 scheduleTraversals(); } }

void scheduleTraversals() { if (!mTraversalScheduled) { // 保證一次 requestLayout 只執行一次View樹的遍歷 mTraversalScheduled = true; // 通過Handler發送同步屏障阻塞同步消息 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 通過Choreographer發出一個mTraversalRunnable,會在這裏執行 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); // ... } } `` 可以看到在調用 ViewRootImpl 的 requestLayout 方法後,會執行scheduleTraversals方法,在這個方法中通過mHandler.getLooper().getQueue().postSyncBarrier()`調用了 MessageQueue 的同步屏障方法,從而插入了一個同步屏障消息。

如果你對 Android 的屏幕繪製流程有一定了解的話,應該知道 Vsync 信號與 Choreographer。 Choreographer 會向系統底層訂閲 Vsync 信號,系統底層會間隔大約16ms(60hz刷新率的屏幕)發送一次 Vsync信號。等到接收到 Vsync 信號後,Choreographer 會回調 ViewRootImpl 的 doTraversal 方法開始 View 樹真正的遍歷與繪製。doTraversal 源碼如下:

```java // ViewRootImpl

final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }

void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; // 移除同步屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

     if (mProfile) {
        Debug.startMethodTracing("ViewAncestor");
     }
     //  通過該方法開啟View的繪製流程,會調用performMeasure方法、performLayout方法和performDraw方法。
     performTraversals();

     if (mProfile) {
        Debug.stopMethodTracing();
        mProfile = false;
     }
  }

} ``` 可以看到在doTraversal方法中會將同步屏障消息移除掉,之後普通消息又會得到執行的機會。其實到這裏也很容易理解為什麼 postSyncBarrier 方法不允許開發者調用了。因為一旦開發者執行這個方法,且沒有即使移除同步屏障就會導致普通消息再也沒有被執行的機會。

(2)普通消息和延遲消息

關於普通消息和延遲消息其實沒有什麼可説的,普通消息的優先級比異步消息低毋庸置疑。而延遲消息由於在插入消息隊列時會根據延遲時間確定插入到隊列中的位置,即延遲越久的消息在隊列中的位置越靠後。因此延遲消息的優先級是最低的。

(3)IdleHandler

除了以上消息的優先級外,還有一種叫 IdelHandler 的消息(這裏冒昧的稱它為消息),IdelHandler 從本質上來説並不是一個 Message,而是一個接口,其源碼如下:

java public static interface IdleHandler { /** * Called when the message queue has run out of messages and will now * wait for more. Return true to keep your idle handler active, false * to have it removed. This may be called if there are still messages * pending in the queue, but they are all scheduled to be dispatched * after the current time. */ boolean queueIdle(); } 通過註釋可以看得出來 queueIdle 方法是在 MessageQueue 中的消息執行完後或者有延遲消息在等待執行時才會被調用。因此可以看得出 IdleHandler 執行的優先級是比異步消息和普通消息低的,但要比延遲消息優先級高。接下來我們看一下IdleHandler的具體源碼實現。

```java public final class MessageQueue {

private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();

public void addIdleHandler(@NonNull IdleHandler handler) {
    if (handler == null) {
        throw new NullPointerException("Can't add a null IdleHandler");
    }
    synchronized (this) {
        mIdleHandlers.add(handler);
    }
}

public void removeIdleHandler(@NonNull IdleHandler handler) {
    synchronized (this) {
        mIdleHandlers.remove(handler);
    }
}

} ``` 可以看到在 MessageQueue 中有一個泛型為 IdleHandler 的 ArrayList 的成員變量,並且提供了addIdleHandler 方法可以向 mIdleHandlers 中添加 IdelHandler,同時也提供了 removeIdleHandler 來移除 IdleHandler。

那接下來繼續看 MessageQueue 的 next 方法中對於 IdleHandler 的處理邏輯。

```java Message next() { // pendingIdleHandlerCount 默認是-1,小於0 int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { // ...

    int pendingIdleHandlerCount = -1;

    synchronized (this) {


        // ... 省略異步消息與普通消息的處理邏輯

        // 如果第一次調用next方法,pendingIdleHandlerCount 一定小於0
        // mMessage == null 説明消息隊列中沒有消息
        // now < mMessages.when 説明有延遲消息,但是還沒有到執行的時間
        if (pendingIdleHandlerCount < 0
                && (mMessages == null || now < mMessages.when)) {
            // 獲取IdleHandler的個數    
            pendingIdleHandlerCount = mIdleHandlers.size();
        }
        // 説明沒有 IdleHandler
        if (pendingIdleHandlerCount <= 0) {
            mBlocked = true;
            跳過下面的邏輯,繼續執行for循環
            continue;
        }

        if (mPendingIdleHandlers == null) {
            mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
        }
        // 將mIdleHandlers中的數據複製到mPendingIdleHandlers數組中
        mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }

    // 遍歷 mPendingIdleHandlers 數組
    for (int i = 0; i < pendingIdleHandlerCount; i++) {
        // 取出遍歷到的IdleHandler
        final IdleHandler idler = mPendingIdleHandlers[i];
        // 將以取出的位置設置為null
        mPendingIdleHandlers[i] = null; // release the reference to the handler

        boolean keep = false;
        try {
            // 執行Idler.queueIdle方法
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }
        // 這裏可以看出 queueIdle 如果返回false,則會將這個IdleHandler從集合中移除,下次就不會再執行了。
        if (!keep) {
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }
}

} ```

代碼中的註釋已經寫得非常詳細了,可以看得出來 IdleHandler 只有在 MessageQueue 沒有消息時或者延遲消息沒有到執行時間時才會執行 IdleHandler 的 queueIdle 方法。並且 queueIdle 方法的返回值確定了是否會將這個 IdleHandler 從集合中移除。

消息機制的設計者設計 IdelHandler 的目的就是為了執行一些不那麼緊急的任務,在異步消息和普通消息執行完後,處於空閒時間時才會開始執行 IdleHandler。

而 IdleHandler 在 Framework 的源碼中也是被頻繁用到的。典型的用法是 Activity 的 onDestroy 生命週期的調用,就是通過向 MessageQueue 中添加 IdleHandler 來實現的。也就是説當 Activity 執行了 finish 方法後並不會立即執行 onDestory 方法,而是要等到消息隊列空閒時 onDestory 才會被調用。

如果是這樣的話,在Activity調用 finish 時,不斷的向 MessageQueue 中插入消息,是不是會導致 Activity 的 onDestory 一直不會被調用呢?理論上是這樣的,但是在系統的源碼中做了一個兜底,即如果finish之後過了十秒 Activity 依然沒有被銷燬則會主動調用 Activity 的 onDestory 來執行銷燬邏輯。

關於 onDestory 部分的源碼分析可以參考路遙的一篇文章《面試官:為什麼 Activity.finish() 之後 10s 才 onDestroy ?》,這裏就不做過多解讀了。

三、總結

本篇文章的內容比較長,文章從 ”生產者-消費者“模型來對比Android消息機制的實現,並嘗試站在設計者的角度分析應該怎樣設計系統的消息機制,還嘗試分析了在實現過程中碰到的問題及解決方案。如果你能細心的看完這篇文章,一定會有所收穫,並且會對 Android 的消息機制有一個全新的理解。

推薦閲讀

《追根溯源—— 探究Handler的實現原理》

《Java併發系列番外篇:ThreadLocal原理其實很簡單》

《深入理解Java線程的等待與喚醒機制》

《面試官:為什麼 Activity.finish() 之後 10s 才 onDestroy ?》