Android 訊息機制:Handler、MessageQueue 和 Looper

WngShhng發表於2019-03-03

在這篇文章中,我們將會討論 Android 的訊息機制。提到 Handler,有過一些 Android 開發經驗的都應該很清楚它的作用,通常我們使用它來通知主執行緒更新 UI。但是 Handler 需要底層的 MessageQueue 和 Looper 來支援才能運作。這篇文章中,我們將會討論它們三個之間的關係以及實現原理。

在這篇文章中,因為涉及執行緒方面的東西,所以就避不開 ThreadLocal。筆者在之前的文章中有分析過該 API 的作用,你可以參考筆者的這篇文章來學習下它的作用和原理,本文中我們就不再專門講解:《Java 併發程式設計:ThreadLocal 的使用及其原始碼實現》

1、Handler 的作用

通常,當我們在非主執行緒當中做了非同步的操作之後使用 Handler 來在主執行緒當中更新 UI。之所以這麼設計無非就是因為 Android 中的 View 不是執行緒安全的。之所以將 View 設計成非執行緒安全的,是因為:1).對 View 進行加鎖之後會增加控制元件使用的複雜度;2).加鎖之後會降低控制元件執行的效率。但 Handler 並非只能用來在主執行緒當中更新 UI,確切來說它有兩個作用:

  1. 任務排程:即通過 post()send() 等方法來指定某個任務在某個時間執行;
  2. 執行緒切換:你也許用過 RxJava,但如果在 Android 中使用的話還要配合 RxAndroid,而這裡的 RxAndroid 內部就使用 Handler 來實現執行緒切換。

下文中,我們就來分別看一下它的這兩個功能的作用和原理。

1.1 任務排程

使用 Hanlder 可以讓一個任務在某個時間點執行或者等待某段時間之後執行。Handler 為此提供了許多方法,從方法的命名上,我們可以將其分成 post()sned() 兩類方法。``post() 類的用來指定某個 Runnable 在某個時間點執行,send() 類的用來指定某個 Message 在某個時間點執行。

這裡的 Message 是 Android 中定義的一個類。它內部有多個欄位,比如 whatarg1arg2replyTosendingUid 來幫助我們指定該訊息的內容和物件。同時, Message 還實現了 Parcelable 介面,這表明它可以被用來跨程式傳輸。此外,它內部還定義了一個 Message 型別的 next 欄位,這表明 Message 可以被用作連結串列的結點。實際上 MessageQueue 裡面只存放了一個 mMessage,即連結串列的頭結點。所以,MessageQueue 內部的訊息佇列,本質上是一個單連結串列,每個連結串列的結點就是 Message

當呼叫 post() 型別的方法來排程某個 Runnable 的時候,首先會將其包裝成一個 Message,然後再使用 send() 類的方法進行任務分發。所以,不論是 post() 類的方法還是 send() 類的方法,最終都會使用 HandlersendMessageAtTime() 方法來將其加入到佇列中:

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            // ... 無關程式碼
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }
複製程式碼

使用 Handler 進行任務排程是非常簡單的。下面的程式碼就實現了讓一個 Runnable 在 500ms 之後執行的邏輯:

    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            // do something
        }
    }, 500);
複製程式碼

上面的任務執行方式在主執行緒中執行不會出現任何問題,如果你在非主執行緒中執行的話就可能會出現異常。原因我們後面會講解。

既然每個 Runnable 被 post() 傳送之後還要被包裝成 Message,那麼 Message 的意義何在呢?

Runnable 被包裝的過程依賴於 Handler 內部的 getPostMessage() 方法。下面是該方法的定義:

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
複製程式碼

可見,我們的 Runnable 會被賦值給 Message 的 callback。這種型別的訊息無法做更詳細的處理。就是說,我們無法利用訊息的 whatarg1 等欄位(本身我們也沒有設定這些欄位)。如果我們希望使用 Message 的這些欄位資訊,就需要:

  1. 首先,要使用 send() 型別的方法來傳遞我們的 Message 給 Handler;
  2. 然後,我們的 Handler 要覆寫 handleMessage() 方法,並在該方法中獲取每個 Message 並根據其內部的資訊依次處理。

下面的一個例子用來演示 send() 型別的方法。首先,我們要定義 Handler 並覆寫其 handleMessage() 方法來處理訊息:

    private final static int SAY_HELLO = 1;

    private static Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SAY_HELLO:
                    LogUtils.d("Hello!");
                    break;
            }
        }
    };
複製程式碼

然後,我們向該 Handler 傳送訊息:

    Message message = Message.obtain(handler);
    message.what = SAY_HELLO;
    message.sendToTarget();
複製程式碼

這樣,我們的 Handler 接收到了訊息並根據其 what 得知要 SAY_HELLO,於是就列印出了日誌資訊。除了呼叫 Message 的 sendToTarget() 方法,我們還可以直接呼叫 handler 的 sendMessage() 方法(sendToTarget() 內部呼叫了 handler 的 sendMessage())。

1.2 執行緒切換

下面我們用了一份示例程式碼,它會先在主執行緒當中例項化一個 Handler,然後在某個方法中,我們開啟了一個執行緒,並執行了某個任務。2 秒之後任務結束,我們來更新 UI。

    // 在主執行緒中獲取 Handler
    private static Handler handler = new Handler();		

    // 更新UI,會將訊息傳送到主執行緒當中
    new Thread(() -> {
        try {
            Thread.sleep(2000);
            handler.post(() -> getBinding().tv.setText("主執行緒更新UI"));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
複製程式碼

上面之所以能夠在主執行緒當中更新 UI,主要是因為我們的 Handler 是在主執行緒當中進行獲取的。隨後,我們呼叫 handlerpost() 方法之後,傳入的 Runnable 會被包裝成 Message,然後加入到主執行緒對應的訊息佇列中去,並由主執行緒對應的 Looper 獲取到並執行。所以,就使得該 Runnable 的操作最終在主執行緒中完成。

也許你會覺得先在主執行緒當中獲取到 Handler 然後再使用比較麻煩。別擔心,我們還有另一種方式來解決這個問題。我們可以直接使用 Looper 的 getMainLooper() 方法來獲取主執行緒對應的 Looper,然後使用它來例項化一個 Handler 並使用該 Handler 來處理訊息:

    new Handler(Looper.getMainLooper())
        .post(() -> getBinding().tv.setText("主執行緒更新UI"));
複製程式碼

本質上,當我們呼叫 Handler 的無參構造方法,或者說不指定 Looper 的構造方法的時候,會直接使用當前執行緒對應的 Looper 來例項化 Handler。每個執行緒對應的 Looper 儲存在該執行緒的區域性變數 ThreadLocal 裡。當某個執行緒的區域性變數裡面沒有 Looper 的時候就會丟擲一個異常。所以,我們之前說直接使用 new 來例項化一個 Handler 的時候可能出錯就是這個原因。

主執行緒對應的 Looper 會在 ActivityThread 的靜態方法 main() 中被建立,它會呼叫 Looper 的 prepareMainLooper() 靜態方法來建立主執行緒對應的 Looper。然後會呼叫 Looper 的 loop() 靜態方法來開啟 Looper 迴圈以不斷處理訊息。這裡的 ActivityThread 用來處理應用程式中的活動和廣播的請求,會在應用啟動的時候呼叫。ActivityThread 內部定義了一個內部類 H,它繼承自 Handler,同樣執行在主執行緒中,用來處理接收到的來自各個活動、廣播和服務的請求。

除了使用主執行緒對應的 Looper,我們也可以開啟我們自定義執行緒的 Looper。下面的程式碼中,我們開啟了一個執行緒,並線上程中先呼叫 Looper 的 prepare() 靜態方法,此時 Looper 會為我們當前的執行緒建立 Looper,然後將其加入到當前執行緒的區域性變數裡面。隨後,當我們呼叫 Looper 的 loop() 方法的時候就開啟了 Looper 迴圈來不斷處理訊息:

    new Thread(() -> {
        LogUtils.d("+++++++++" + Thread.currentThread());
        Looper.prepare();
        new Handler().post(() -> LogUtils.d("+++++++++" + Thread.currentThread()));
        Looper.loop();
    }).start();
複製程式碼

從以上的內容我們可以看出,Handler 之所以能夠實現執行緒切換,主要的原因是其內部的訊息佇列是對應於每一個執行緒的。傳送的任務會在該執行緒對應的訊息佇列中被執行。而成功獲取到該執行緒對應的訊息佇列就依靠 ThreadLocal 來對每個執行緒對應的訊息佇列進行儲存。

2、原始碼解析

以上,我們分析了 Handler 的主要的兩種主要用途,並且在這個過程中,我們提及了許多 Handler、MessageQueue 和 Looper 的底層設計。在上面的文章中,我們只是使用了文字來進行描述。在下文中,我們來通過原始碼來驗證我們上面提到的一些內容。

2.1 例項化 Handler

Handler 了提供了多個過載的構造方法,我們可以將其分成兩種主要的型別。一種在構造方法中需要明確指定一個 Looper,另一種在構造方法中不需要指定任何 Looper,在構造方法內部會獲取當前執行緒對應的 Looper 來初始化 Handler。

第一種初始化的方式最終都會呼叫下面的方法來完成初始化。這個方法比較簡單,是基本的賦值操作:

    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
複製程式碼

第二種初始化的方式最終會呼叫下面的方法。這裡使用 Looper 的靜態方法 myLooper() 來獲取當前執行緒對應的 Looper。如果當前執行緒不存在任何 Looper 就會丟擲一個異常。

    public Handler(Callback callback, boolean async) {
        // 潛在記憶體洩漏的檢查
        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());
            }
        }

        // 使用 Looper 的靜態方法 myLooper() 來獲取當前執行緒的 Looper
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException();
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
複製程式碼

而 Looper 的靜態方法 myLooper() 會使用執行緒區域性變數 sThreadLocal 來獲取之前儲存到該執行緒內部的 Looper:

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

2.2 Looper 的初始化

前面我們也說過 Looper 的建立過程。對於主執行緒的 Looper 會在 ActivityThreadmain() 方法中被呼叫:

    public static void main(String[] args) {
        // ... 無關程式碼
        Looper.prepareMainLooper();
        // ... 無關程式碼
        // 開啟 Looper 迴圈
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
複製程式碼

這裡呼叫了 Looper 的靜態方法 prepareMainLooper() 來初始化主執行緒的 Looper:

    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
複製程式碼

其內部先呼叫了 prepare(boolean) 方法來初始化一個 Looper 並將其放線上程區域性變數 sThreadLocal 中,然後判斷 sMainLooper 是否之前存在過。這是一種基本的單例校驗,顯然,我們只允許主執行緒的 Looper 被例項化一次。

同樣,非主執行緒的 Looper 也只允許被例項化一次。當我們在非主執行緒例項化一個 Looper 的時候會呼叫它的 prepare() 靜態方法。它同樣呼叫了 prepare(boolean) 方法來初始化一個 Looper 並將其放線上程區域性變數 sThreadLocal 中。所以,主執行緒和非主執行緒的 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));
    }
複製程式碼

經過上述分析,我們可以得知,對於一個執行緒只能例項化一個 Looper,所以當我們在同一個執行緒中多次建立 Handler 例項,它們是共享一個 Looper 的。或者說是一個 Looper 對應多個 Handler 也是可以的。

2.3 MessageQueue 的例項化

相比於 Looper 和 Handler,MessageQueue 就顯得相對複雜一些。因為內部用到了 JNI 程式設計。初始化、銷燬和入隊等事件都用到了 native 的方法。你可以在 android_os_MessageQueue 檢視其原始碼的定義。

每當我們例項化一個 Looper 的時候會呼叫它的構造方法,並在其中例項化一個 MessageQueue:

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

在例項化 Handler 的小節中可以看出,每次例項化一個 Handler 的時候,會從當前執行緒對應的 Looper 中取出 MessageQueue。所以,這裡我們又可以得出結論一個 Handler 對應一個 MessageQueue。

當我們例項化一個 MessageQueue 的時候會使用它的構造方法。這裡會呼叫 native 層的 nativeInit() 方法來完成 MessageQueue 的初始化:

    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }
複製程式碼

在 native 層,nativeInit() 方法的定義如下:

    static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
        NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
        if (!nativeMessageQueue) {
            jniThrowRuntimeException(env, "Unable to allocate native queue");
            return 0;
        }
        nativeMessageQueue->incStrong(env);
        return reinterpret_cast<jlong>(nativeMessageQueue);
    }
複製程式碼

從上面我們可以看出,在該方法中例項化了一個 NativeMessageQueue 之後返回了 mPtr 作為是 Java 層 MessageQueue 與NativeMessesageQueue 的橋樑。這個 long 型別的成員儲存了 native 例項,這是 jni 開發中常用到的方式。因此 MessageQueue 同樣使用 mPtr 來表示 native 層的訊息佇列。NativeMessageQueue 在 native 層的部分定義和其構造方法的定義如下。

    class NativeMessageQueue : public MessageQueue, public LooperCallback {
    // ... 無關程式碼
    NativeMessageQueue::NativeMessageQueue() :
            mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
        mLooper = Looper::getForThread();
        if (mLooper == NULL) {
            mLooper = new Looper(false);
            Looper::setForThread(mLooper);
        }
    }
複製程式碼

從上面我們可以看出,NativeMessageQueue 繼承自 MessageQueue。並且在其內部例項化了一個 native 層的 Looper(其原始碼在 Looper)。

在 Android 的 native 層存在著一個於 Java 層類似的 Looper,它的主要作用是用來與 Java 層的 Looper 相互配合完成 Android 中最主要的執行緒通訊。當訊息佇列中有訊息存入時,會喚醒 Natvice 層的 Looper。當訊息佇列中沒有訊息時或者訊息尚未到處理時間時, Natvice 層的 Looper 會 block 住整個執行緒。所以,建立了 Java Looper 的執行緒只有在有訊息待處理時才處於活躍狀態,無訊息時 block 在等待訊息寫入的狀態。既然如此,當我們在主執行緒中開啟了 Looper 迴圈的話,為什麼不會 block 住整個執行緒而導致 ANR 呢?這是因為,我們的主執行緒的訊息都會傳送給主執行緒對應的 Looper 來處理,所以,本質上,我們主執行緒中的許多事件也都是以訊息的形式傳送給主執行緒的 Handler 來進行處理的。只有當某個訊息被執行的時間過長以至於無法處理其他事件的時候才會出現 ANR。

上面我們例項化了一個 Native 層的 Looper。在其中主要做到的邏輯如下:

    void Looper::rebuildEpollLocked() {
        // 如果之前存在的話就關閉之前的 epoll 例項
        if (mEpollFd >= 0) {
            mEpollFd.reset(); // 關閉舊的epoll例項
        }
        // 申請新的 epoll 例項,並且註冊 “Wake管道”
        mEpollFd.reset(epoll_create(EPOLL_SIZE_HINT));
        LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));
        struct epoll_event eventItem;
        // 把未使用的資料區域進行置0操作
        memset(& eventItem, 0, sizeof(epoll_event));
        eventItem.events = EPOLLIN;
        eventItem.data.fd = mWakeEventFd.get();
        // 將喚醒事件 (mWakeEventFd) 新增到 epoll 例項 (mEpollFd)
        int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem);
        LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake event fd to epoll instance: %s", strerror(errno));
        // 這裡主要新增的是Input事件如鍵盤,感測器輸入,這裡基本上由系統負責,很少主動去新增
        for (size_t i = 0; i < mRequests.size(); i++) {
            const Request& request = mRequests.valueAt(i);
            struct epoll_event eventItem;
            request.initEventItem(&eventItem);
            // 將 request 佇列的事件,分別新增到 epoll 例項
            int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem);
        }
    }
複製程式碼

這裡涉及了 epoll 相關的知識。epoll 是一個可擴充套件的 Linux I/O 事件通知機制,用來實現多路複用 (Multiplexing)。它將喚醒事件按對應的 fd 註冊進 epoll,然後 epoll 幫你監聽哪些喚醒事件上有訊息到達。此時的喚醒事件應該採用非阻塞模式。這樣,整個過程只在呼叫 epoll 的時候才會阻塞,收發客戶訊息是不會阻塞的,整個程式或者執行緒就被充分利用起來,這就是事件驅動,所謂的響應模式。

上面的程式碼中使用了 epoll_ctl 方法來將被監聽的描述符新增到 epoll 控制程式碼。關於 epoll 的指令,可以參考這篇博文 《epoll機制:epoll_create、epoll_ctl、epoll_wait、close》。這部分程式碼的主要作用是建立一個 epoll 例項並用它來監聽 event 觸發。

2.4 訊息的執行過程

2.4.1 訊息入隊的過程

在介紹 Handler 的使用的時候,我們也說過不論是 Runnable 還是 Message 最終都會被封裝成 Meseage 並加入到佇列中。那麼,加入佇列之後又是怎麼被執行的呢?

首先,我們先看下入隊的過程。以下是 Handler 中定義的方法,每當我們將一個訊息入隊的時候,都會呼叫它來完成。

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
複製程式碼

從上面可以看出,入隊的時候實際上是使用了 MessageQueue 的 enqueueMessage() 方法。所以,我們再來看下該方法的定義:

    boolean enqueueMessage(Message msg, long when) {
        // ... 無關程式碼,校驗
        synchronized (this) {
            // ... 無關程式碼
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p;
                prev.next = msg;
            }

            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
複製程式碼

從上面的方法可以看出,所謂的入隊操作本質上就是一個將新的訊息加入到佇列中的邏輯。當然,這裡加入的時候要根據訊息的觸發時間對訊息進行排序。然後,會根據 needWake 來決定是否呼叫 native 層的方法進行喚醒。只有噹噹前的頭結點訊息之前存在柵欄 (barrier) 並且新插入的訊息是最先要被觸發的非同步訊息就進行喚醒。當一般情況下是無需進行喚醒的。

這裡的 nativeWake() 方法會最終呼叫 native 層的 Looper 的 awake() 方法:

    void Looper::wake() {
        uint64_t inc = 1;
        ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
        if (nWrite != sizeof(uint64_t)) {
            if (errno != EAGAIN) {
                LOG_ALWAYS_FATAL("Could not write wake signal to fd %d: %s", mWakeEventFd.get(), strerror(errno));
            }
        }
    }
複製程式碼

此方法向 mWakeEventFd 寫入了一個位元組的內容。到底是什麼內容並不重要,重要的是 fd 存在內容了,換句話說就是 mWakeEventFd 可讀了,也就是 Native 層的 Looper 的執行緒從 block 狀態中醒了過來。之所以需要進行喚醒,是因為,每次我們處理了訊息之後會根據下個訊息執行的時間進行喚醒。如果新插入的訊息是最新的訊息,那麼顯然,我們需要把喚醒的時間重置。(Native 層的 Looper 會在我們呼叫 Java 層的 MessageQueue 的時候執行 epoll_wait 時進入 block 狀態。)

2.4.2 訊息執行的過程

在上文中,我們分析了 MessageQueue 將訊息入隊的過程。那麼這些訊息要在什麼時候被執行呢?在介紹 Handler 的使用的時候,我們也提到過當我們例項化了 Looper 之後都應該呼叫它的 loop() 靜態方法來處理訊息。下面我們來看下這個方法的定義。

    public static void loop() {
        final Looper me = myLooper();
        // .. 無關程式碼
        final MessageQueue queue = me.mQueue;
        // .. 無關程式碼
        for (;;) {
            Message msg = queue.next(); // 可能會 bolck
            if (msg == null) {
                return;
            }
            // ... 無關程式碼
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            // ... 無關程式碼
            msg.recycleUnchecked();
        }
    }
複製程式碼

從上面我們可以看出,當該方法被呼叫的時候,它會先開啟一個無限迴圈,並在該迴圈中使用 MessageQueue 的 next() 方法來取出下一個訊息並進行分發。這裡我們先不看 next() 方法的定義。我們先把這個方法中涉及的部分分析一下。

當獲取到了下一個訊息之後,會呼叫它的target 也就是傳送該訊息的 Handler 的 dispatchMessage() 方法來進行處理。該方法的定義如下:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
複製程式碼

從上面可以看出,如果該訊息是通過包裝 Runnable 得到的話,會直接呼叫它的 handleCallback() 方法進行處理。在該方法內部會直接呼叫 Runnable 的 run() 方法。因為比較見到那,我們就補貼出程式碼了。

然後,會根據 mCallback 是否為空來決定是交給 mCallback 進行處理還是內部的 handleMessage() 方法。這裡的 mCallback 是一個介面,可以在建立 Handler 的時候通過構造方法指定,也比較簡單。而這裡的 handleMessage() 方法,我們就再熟悉不過了,它就是我們建立 Handler 的時候重寫的、用來處理訊息的方法。這樣,訊息就被髮送到了我們的 Handler 中進行處理了。

以上就是訊息被處理的過程,程式碼的邏輯還是比較清晰的。下面我們就來看下 MessageQueue 是如何獲取 “下一個” 訊息的。

2.4.3 MessageQueue 的訊息管理

上面我們已經分析完了 Handler 傳送的訊息執行的過程。這裡我們在來分析一下其中的獲取 “下一個” 訊息的邏輯:

    Message next() {
        // 如果訊息迴圈已經停止就直接返回。如果應用嘗試重啟已經停止的Looper就會可能發生這種情況。
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            // 呼叫 native 的方法,可能會這個函式發生 block
            nativePollOnce(ptr, nextPollTimeoutMillis);
            // ... 無關程式碼
        }
    }
複製程式碼

從上面可以看出 Java 層的 MessageQueue 的 next() 方法是一個迴圈。除了獲取訊息佇列之外,還要監聽 Natvie 層 Looper 的事件觸發。通過呼叫 native 層的 nativePollOnce() 方法來實現。該方法內部又會呼叫 NativeMessageQueuepollOnce() 方法。而且注意下,在下面的方法中,nativeMessageQueue 是從 Java 層的 mPtr 中獲取到的。所以我們說,在初始化 MessageQueue 的時候得到的 mPtr 起到了橋樑的作用:

    static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
            jlong ptr, jint timeoutMillis) {
        NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
        nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
    }
複製程式碼

NativeMessageQueuepollOnce() 方法中會呼叫 native 層的 LooperpollOnce(),並最終呼叫 native 層 Looper 的 pollInner() 方法:

    int Looper::pollInner(int timeoutMillis) {
        // ... 根據下一個訊息的事件調整超時時間
        int result = POLL_WAKE;
        mResponses.clear();
        mResponseIndex = 0;
        mPolling = true; // 將要空閒
        struct epoll_event eventItems[EPOLL_MAX_EVENTS];
        // 待已註冊之事件被觸發或計時終了
        int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
        mPolling = false; // 不再空閒
        mLock.lock(); // 請求鎖
        if (mEpollRebuildRequired) {
            mEpollRebuildRequired = false;
            rebuildEpollLocked(); // 根據需要重建 epoll
            goto Done;
        }
        // 進行檢查
        if (eventCount < 0) {
            if (errno == EINTR) {
                goto Done;
            }
            result = POLL_ERROR; // 錯誤
            goto Done;
        }
        if (eventCount == 0) {
            result = POLL_TIMEOUT; // 超時
            goto Done;
        }
        // 處理所有訊息
        for (int i = 0; i < eventCount; i++) {
            int fd = eventItems[i].data.fd;
            uint32_t epollEvents = eventItems[i].events;
            if (fd == mWakeEventFd.get()) { // 喚醒 fd 有反應
                if (epollEvents & EPOLLIN) {
                    awoken(); // 已經喚醒了,則讀取並清空管道資料
                }
            } else {
                // 其他 input fd 處理,其實就是將活動 fd 放入到 responses 佇列中,等待處理
                ssize_t requestIndex = mRequests.indexOfKey(fd);
                if (requestIndex >= 0) {
                    int events = 0;
                    if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
                    if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
                    if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
                    if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
                    // 將訊息放進 mResponses 中
                    pushResponse(events, mRequests.valueAt(requestIndex));
                }
            }
        }
    Done: ;
        // 觸發所有的訊息回撥,處理 Native 層的Message
        mNextMessageUptime = LLONG_MAX;
        while (mMessageEnvelopes.size() != 0) {
            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
            const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
            if (messageEnvelope.uptime <= now) {
                { // 獲取 handler
                    sp<MessageHandler> handler = messageEnvelope.handler;
                    Message message = messageEnvelope.message;
                    mMessageEnvelopes.removeAt(0);
                    mSendingMessage = true;
                    mLock.unlock();
                    handler->handleMessage(message);
                } // 釋放 handler
                mLock.lock();
                mSendingMessage = false;
                result = POLL_CALLBACK;
            } else {
                // 佇列頭部的訊息決定了下個喚醒的時間
                mNextMessageUptime = messageEnvelope.uptime;
                break;
            }
        }
        mLock.unlock(); // 釋放鎖
        // 觸發所有的響應回撥
        for (size_t i = 0; i < mResponses.size(); i++) {
            Response& response = mResponses.editItemAt(i);
            if (response.request.ident == POLL_CALLBACK) {
                int fd = response.request.fd;
                int events = response.events;
                void* data = response.request.data;
                int callbackResult = response.request.callback->handleEvent(fd, events, data);
                if (callbackResult == 0) {
                    removeFd(fd, response.request.seq); // 移除檔案描述符
                }
                response.request.callback.clear();
                result = POLL_CALLBACK;
            }
        }
        return result;
    }
複製程式碼

從上面我們可以看出 Native 層的 pollInner() 方法首先會根據 Java 層傳入的 timeoutMillis 呼叫 epoll_wait 方法來讓執行緒進入等待狀態。如果 timeoutMillis 不為 0,那麼執行緒將進入等待狀態。如果有事件觸發發生,wake 或者其他複用 Looper 的 event,處理event,這樣整個 Native 層的 Looper 將從 block 狀態中解脫出來了。這樣回到 Java 層就將繼續執行 MessageQueue 中下一條語句。至於 Native 層的 Looper 何時從 block 狀態中醒過來,就需要根據我們入隊的訊息來定。也就用到了 MessageQueue 的 enqueueMessage() 方法的最後幾行程式碼:

    if (needWake) {
        nativeWake(mPtr);
    }
複製程式碼

即:只有噹噹前的頭結點訊息之前存在柵欄 (barrier) 並且新插入的訊息是最先要被觸發的非同步訊息就進行喚醒。

上面主要是 Native 層的 Looper 執行緒 block 的相關的邏輯。即當我們獲取訊息佇列的下一條訊息的時候會根據下一個訊息的時間來決定執行緒 block 的時長。當我們將一個訊息加入到佇列的時候會根據新的訊息的時間重新調整執行緒 block 的時長,如果需要的話還需要喚起 block 的執行緒。當執行緒從 block 狀態恢復出來的時候,Java 層的 Looper 就拿到了一個訊息,對該訊息進行處理即可。

3、總結

在上文中,我們從 Java 層到 Native 層分析了 Handler 的作用的原理。這裡我們對這部分內容做一個總結。

3.1 Handler、MessageQueue 和 Looper 之間的關係

首先是 Handler、MessageQueue 和 Looper 之間的關係。我們用下面的這個圖來表示:

MessageQueue Handler Looper

也就是說,一個執行緒中可以定義多個 Handler 例項,但是每個 Handler 實際上引用的是同一個 Looper。當然,我們要在建立 Handler 之前先建立 Looper。而每個 Looper 又只對應一個 MessageQueue。該 MessageQueue 會在建立 Looper 的時候被建立。在 MessageQueue 中使用 Message 物件來拼接一個單向的連結串列結構,依次來構成一個訊息佇列。每個 Message 是連結串列的一個結點,封裝了我們傳送的資訊。

3.2 Handler 的訊息傳送過程

然後,我們再來分析下 Handler 中的訊息是如何被髮送的。同樣,我們使用一個圖來進行分析:

Handler 的訊息傳送過程

根據上文的內容我們將 Handler 傳送訊息的方法分成 post 和 send 兩種型別。post 的用來傳送 Runnable 型別的資料,send 型別的用來傳送 Message 型別的資料。但不論哪種型別最終都會呼叫 Handler 的 sendMessageAtTime() 方法來加入到 MessageQueue 的佇列中。區別在於,post 型別的方法需要經過 Handler 的 getPostMessage() 包裝成 Message 之後再傳送。

3.3 Looper 的執行過程

當訊息被新增到佇列之後需要執行訊息,這部分內容在 Looper 的 loop() 方法中。但是這部分內容稍顯複雜,因為涉及 Native 層的一些東西。我們這裡仍然使用圖來進行描述:

Looper 的執行過程

當我們呼叫 Looper 的 loop() 方法之後整個 Looper 迴圈就開始不斷地處理訊息了。在上圖中就是我們用綠色標記的一個迴圈。當我們在迴圈中呼叫 MessageQueue 的 next() 方法來獲取下一個訊息的時候,會呼叫 nativePollOnce() 方法,該方法可能會造成執行緒阻塞和非阻塞,當執行緒為非阻塞的時候就會從 Native 層回到 Java 層,從 MessageQueuue 中取得一個訊息之後給 Looper 進行處理。如果獲取的時候造成執行緒阻塞,那麼有兩種情況會喚醒阻塞的執行緒,一個是當一個新的訊息被加入到佇列中,並且將會早於之前佇列的所有訊息被觸發,那麼此時將會重新設定超時時間。如果達到了超時時間同樣可以從睡眠狀態中返回,也就回到了 Java 層繼續處理。所以,Native 層的 Looper 的作用就是通過阻塞訊息佇列獲取訊息的過程阻塞 Looper。

3.4 最後

因為本文中不僅分析了 Java 層的程式碼,同時分析了 framework 層的程式碼,所以最好能夠結合兩邊的原始碼一起看,這樣更有助於自己的理解。在上面的文章中,我們給出了一些類的線上的程式碼連結,在 Google Source 上面,需要 VPN 才能瀏覽。另外,因為筆者水平有限,難免存在有誤和不足的地方,歡迎批評指正。


我是 WngShhng. 如果您喜歡我的文章,可以在以下平臺關注我:

相關文章