反思 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 ?》