一文讀懂 Handler 機制全家桶

葉志陳發表於2020-12-06

Handler 在整個 Android 開發體系中佔據著很重要的地位,對開發者來說起到的作用很明確,就是為了實現執行緒切換或者是執行延時任務,稍微更高階一點的用法可能是為了保證多個任務在執行時的有序性。由於 Android 系統中的主執行緒有特殊地位,所以像 EventBus 和 Retrofit 這類並非 Android 獨有的三方庫,都是通過 Handler 來實現對 Android 系統的特殊平臺支援。大部分開發者都已經對如何使用 Handler 很熟悉了,這裡就再來了解下其內部具體是如何實現的

本文基於 Android API 30(即 Android 11)的系統原始碼進行講解

一、動手實現 Handler

本文不會一上來就直接介紹原始碼,而是會先根據我們想要實現的效果來反推原始碼,一步步來自己動手實現一個簡單的 Handler

1、Message

首先,我們需要有個載體來表示要執行的任務,就叫它 Message 吧,Message 應該有什麼引數呢?

  • 需要有一個唯一標識,因為要執行的任務可能有多個,我們要分得清哪個是哪個,用個 Int 型別變數就足夠表示了
  • 需要能夠承載資料,需要傳送的資料型別會有很多種可能,那就直接用一個 Object 型別變數來表示吧,由開發者自己在使用時再來強轉型別
  • 需要有一個 long 型別變數來表示任務的執行時間戳

所以,Message 類就應該至少包含以下幾個欄位:

/**
 * @Author: leavesC
 * @Date: 2020/12/1 13:31
 * @Desc:
 * GitHub:https://github.com/leavesC
 */
public final class Message {
    //唯一標識
    public int what;
    //資料
    public Object obj;
    //時間戳
    public long when;
}

2、MessageQueue

因為 Message 並不是傳送了就能夠馬上被消費掉,所以就肯定要有個可以用來存放的地方,就叫它 MessageQueue 吧,即訊息佇列。Message 可能需要延遲處理,那麼 MessageQueue 在儲存 Message 的時候就應該按照時間戳的大小來順序存放,時間戳小的 Message 放在佇列的頭部,在消費 Message 的時候就直接從佇列頭取值即可

那麼用什麼資料結構來存放 Message 比較好呢?

  • 用陣列?不太合適,陣列雖然在遍歷的時候會比較快,但需要預先就申請固定的記憶體空間,導致在插入資料和移除資料時可能需要移動大量資料。而 MessageQueue 可能隨時會收到數量不定、時間戳大小不定的 Message,消費完 Message 後還需要將該其移出佇列,所以使用陣列並不合適
  • 用連結串列?好像可以,連結串列在插入資料和移除資料時只需要改變指標的引用即可,不需要移動資料,記憶體空間也只需要按需申請即可。雖然連結串列在隨機訪問的時候效能不高,但是對於 MessageQueue 而言無所謂,因為在消費 Message 的時候也只需要取佇列頭的值,並不需要隨機訪問

好了,既然決定用連結串列結構,那麼 Message 就需要增加一個欄位用於指向下一條訊息才行

/**
 * @Author: leavesC
 * @Date: 2020/12/1 13:31
 * @Desc:
 * GitHub:https://github.com/leavesC
 */
public final class Message {
    //唯一標識
    public int what;
    //資料
    public Object obj;
    //時間戳
    public long when;
	//下一個節點
    public Message next;
}

MessageQueue 需要提供一個 enqueueMessage方法用來向連結串列插入 Message,由於存在多個執行緒同時向佇列傳送訊息的可能,所以方法內部還需要做下執行緒同步才行

/**
 * @Author: leavesC
 * @Date: 2020/12/1 13:31
 * @Desc:
 * GitHub:https://github.com/leavesC
 */
public class MessageQueue {

    //連結串列中的第一條訊息
    private Message mMessages;
    
    void enqueueMessage(Message msg, long when) {
        synchronized (this) {
            Message p = mMessages;
            //如果連結串列是空的,或者處於隊頭的訊息的時間戳比 msg 要大,則將 msg 作為連結串列頭部
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
            } else {
                Message prev;
                //從連結串列頭向連結串列尾遍歷,尋找連結串列中第一條時間戳比 msg 大的訊息,將 msg 插到該訊息的前面
                for (; ; ) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                }
                msg.next = p;
                prev.next = msg;
            }
        }
    }
}

此外,MessageQueue 要有一個可以獲取隊頭訊息的方法才行,就叫做next()吧。外部有可能會隨時向 MessageQueue 傳送 Message,next()方法內部就直接來開啟一個無限迴圈來反覆取值吧。如果當前隊頭的訊息可以直接處理的話(即訊息的時間戳小於或等於當前時間),那麼就直接返回隊頭訊息。而如果隊頭訊息的時間戳比當前時間還要大(即隊頭訊息是一個延時訊息),那麼就計算當前時間和隊頭訊息的時間戳的差值,計算 next() 方法需要阻塞等待的時間,呼叫 nativePollOnce()方法來等待一段時間後再繼續迴圈遍歷

    //用來標記 next() 方法是否正處於阻塞等待的狀態
    private boolean mBlocked = false;

    Message next() {
        int nextPollTimeoutMillis = 0;
        for (; ; ) {
            nativePollOnce(nextPollTimeoutMillis);
            synchronized (this) {
                //當前時間
                final long now = SystemClock.uptimeMillis();
                
                Message msg = mMessages;
                if (msg != null) {
                    if (now < msg.when) {
                        //如果當前時間還未到達訊息的的處理時間,那麼就計算還需要等待的時間
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //可以處理隊頭的訊息了,第二條訊息成為隊頭
                        mMessages = msg.next;
                        msg.next = null;
                        mBlocked = false;
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
                mBlocked = true;
            }
        }
    }

    //將 next 方法的呼叫執行緒休眠指定時間
    private void nativePollOnce(long nextPollTimeoutMillis) {

    }

此時就需要考慮到一種情形:next()還處於阻塞狀態的時候,外部向訊息佇列插入了一個可以立即處理或者是阻塞等待時間比較短的 Message。此時就需要喚醒休眠的執行緒,因此 enqueueMessage還需要再改動下,增加判斷是否需要喚醒next()方法的邏輯

	void enqueueMessage(Message msg, long when) {
        synchronized (this) {
            //用於標記是否需要喚醒 next 方法
            boolean needWake = false;         
            Message p = mMessages;
            //如果連結串列是空的,或者處於隊頭的訊息的時間戳比 msg 要大,則將 msg 作為連結串列頭部
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;     
                //需要喚醒
                needWake = mBlocked;
            } else {
                Message prev;
                //從連結串列頭向連結串列尾遍歷,尋找連結串列中第一條時間戳比 msg 大的訊息,將 msg 插到該訊息的前面
                for (; ; ) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                }
                msg.next = p;
                prev.next = msg;
            }  
            if (needWake) {
                //喚醒 next() 方法
                nativeWake();
            }
        }
    }

    //喚醒 next() 方法
    private void nativeWake() {

    }

3、Handler

既然存放訊息的地方已經確定就是 MessageQueue 了,那麼自然就還需要有一個類可以用來向 MessageQueue 傳送訊息了,就叫它 Handler 吧。Handler 可以實現哪些功能呢?

  • 希望除了可以傳送 Message 型別的訊息外還可以傳送 Runnable 型別的訊息。這個簡單,Handler 內部將 Runnable 包裝為 Message 即可
  • 希望可以傳送延時訊息,以此來執行延時任務。這個也簡單,用 Message 內部的 when 欄位來標識希望任務執行時的時間戳即可
  • 希望可以實現執行緒切換,即從子執行緒傳送的 Message 可以在主執行緒被執行,反過來也一樣。這個也不難,子執行緒可以向一個特定的 mainMessageQueue 傳送訊息,然後讓主執行緒負責迴圈從該佇列中取訊息並執行即可,這樣不就實現了執行緒切換了嗎?

所以說,Message 的定義和傳送是由 Handler 來完成的,但 Message 的分發則可以交由其他執行緒來完成

根據以上需求:Runnable 要能夠包裝為 Message 型別,Message 的處理邏輯要交由 Handler 來定義,所以 Message 就還需要增加兩個欄位才行

/**
 * @Author: leavesC
 * @Date: 2020/12/1 13:31
 * @Desc:
 * GitHub:https://github.com/leavesC
 */
public final class Message {
    //唯一標識
    public int what;
    //資料
    public Object obj;
    //時間戳
    public long when;
	//下一個節點
    public Message next;
	//用於將 Runnable 包裝為 Message
    public Runnable callback;
    //指向 Message 的傳送者,同時也是 Message 的最終處理者
    public Handler target;
}

Handler 至少需要包含幾個方法:用於傳送 Message 和 Runnable 的方法、用來處理訊息的 handleMessage 方法、用於分發訊息的 dispatchMessage方法

/**
 * @Author: leavesC
 * @Date: 2020/12/1 13:31
 * @Desc:
 * GitHub:https://github.com/leavesC
 */
public class Handler {

    private MessageQueue mQueue;
    
    public Handler(MessageQueue mQueue) {
        this.mQueue = mQueue;
    }

    public final void post(Runnable r) {
        sendMessageDelayed(getPostMessage(r), 0);
    }

    public final void postDelayed(Runnable r, long delayMillis) {
        sendMessageDelayed(getPostMessage(r), delayMillis);
    }

    public final void sendMessage(Message r) {
        sendMessageDelayed(r, 0);
    }

    public final void sendMessageDelayed(Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

    public void sendMessageAtTime(Message msg, long uptimeMillis) {
        msg.target = this;
        mQueue.enqueueMessage(msg, uptimeMillis);
    }

    private static Message getPostMessage(Runnable r) {
        Message m = new Message();
        m.callback = r;
        return m;
    }

    //由外部來重寫該方法,以此來消費 Message
    public void handleMessage(Message msg) {

    }

    //用於分發訊息
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            msg.callback.run();
        } else {
            handleMessage(msg);
        }
    }

}

之後,子執行緒就可以像這樣來使用 Handler 了:將子執行緒持有的 Handler 物件和主執行緒關聯的 mainMessageQueue 繫結在一起,主執行緒負責迴圈從 mainMessageQueue 取出 Message 後再來呼叫 Handler 的 dispatchMessage 方法,以此實現執行緒切換的目的

        Handler handler = new Handler(mainThreadMessageQueue) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 1: {
                        String ob = (String) msg.obj;
                        break;
                    }
                    case 2: {
                        List<String> ob = (List<String>) msg.obj;
                        break;
                    }
                }
            }
        };
        Message messageA = new Message();
        messageA.what = 1;
        messageA.obj = "https://github.com/leavesC";
        Message messageB = new Message();
        messageB.what = 2;
        messageB.obj = new ArrayList<String>();
        handler.sendMessage(messageA);
        handler.sendMessage(messageB);

4、Looper

現在就再來想想怎麼讓 Handler 拿到和主執行緒關聯的 MessageQueue,以及主執行緒怎麼從 MessageQueue 獲取 Message 並回撥 Handler。這之間就一定需要一箇中轉器,就叫它 Looper 吧。Looper 具體需要實現什麼功能呢?

  • 每個 Looper 物件應該都是對應一個獨有的 MessageQueue 例項和 Thread 例項,這樣子執行緒和主執行緒才可以互相傳送 Message 交由對方執行緒處理
  • Looper 內部需要開啟一個無限迴圈,其關聯的執行緒就負責從 MessageQueue 迴圈獲取 Message 進行處理
  • 因為主執行緒較為特殊,所以和主執行緒關聯的 Looper 物件要能夠被子執行緒直接獲取到,可以考慮將其作為靜態變數存著

這樣,Looper 的大體框架就出來了。通過 ThreadLocal 來為不同的執行緒單獨維護一個 Looper 例項,每個執行緒通過 prepare()方法來初始化本執行緒獨有的 Looper 例項 ,再通過 myLooper()方法來獲取和當前執行緒關聯的 Looper 物件,和主執行緒關聯的 sMainLooper 作為靜態變數存在,方便子執行緒獲取

/**
 * @Author: leavesC
 * @Date: 2020/12/1 13:31
 * @Desc:
 * GitHub:https://github.com/leavesC
 */
final class Looper {

    final MessageQueue mQueue;

    final Thread mThread;

    private static Looper sMainLooper;

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

    private Looper() {
        mQueue = new MessageQueue();
        mThread = Thread.currentThread();
    }

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

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

    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

    public static Looper myLooper() {
        return sThreadLocal.get();
    }

}

Looper 還需要有一個用於迴圈從 MessageQueue 獲取訊息並處理的方法,就叫它loop()吧。其作用也能簡單,就是迴圈從 MessageQueue 中取出 Message,然後將 Message 再反過來分發給 Handler 即可

	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;
        for (; ; ) {
            Message msg = queue.next();//可能會阻塞
            msg.target.dispatchMessage(msg);
        }
    }

這樣,主執行緒就先通過呼叫prepareMainLooper()來完成 sMainLooper 的初始化,然後呼叫loop()開始向 mainMessageQueue 迴圈取值並進行處理,沒有訊息的話主執行緒就暫時休眠著。子執行緒拿到 sMainLooper 後就以此來初始化 Handler,這樣子執行緒向 Handler 傳送的訊息就都會被存到 mainMessageQueue 中,最終在主執行緒被消費掉

5、做一個總結

這樣一步步走下來後,讀者對於 Message、MessageQueue、Handler、Looper 這四個類的定位就應該都很清晰了吧?不同執行緒之間就可以依靠拿到對方的 Looper 來實現訊息的跨執行緒處理了

例如,對於以下程式碼,即使 Handler 是在 otherThread 中進行初始化,但 handleMessage 方法最終是會在 mainThread 被呼叫執行的,

	    Thread mainThread = new Thread() {
            @Override
            public void run() {
                //初始化 mainLooper
                Looper.prepareMainLooper();
                //開啟迴圈
                Looper.loop();
            }
        };

        Thread otherThread = new Thread() {
            @Override
            public void run() {
                Looper mainLooper = Looper.getMainLooper();
                Handler handler = new Handler(mainLooper.mQueue) {
                    @Override
                    public void handleMessage(Message msg) {
                        switch (msg.what) {
                            case 1: {
                                String ob = (String) msg.obj;
                                break;
                            }
                            case 2: {
                                List<String> ob = (List<String>) msg.obj;
                                break;
                            }
                        }
                    }
                };
                Message messageA = new Message();
                messageA.what = 1;
                messageA.obj = "https://github.com/leavesC";
                Message messageB = new Message();
                messageB.what = 2;
                messageB.obj = new ArrayList<String>();
                handler.sendMessage(messageA);
                handler.sendMessage(messageB);
            }
        };

再來做個簡單的總結:

  • Message:用來表示要執行的任務
  • Handler:子執行緒持有的 Handler 如果繫結到的是主執行緒的 MessageQueue 的話,那麼子執行緒傳送的 Message 就可以由主執行緒來消費,以此來實現執行緒切換,執行 UI 更新操作等目的
  • MessageQueue:即訊息佇列,通過 Handler 傳送的訊息並非都是立即執行的,需要先按照 Message 的優先順序高低(延時時間的長短)儲存到 MessageQueue 中,之後再來依次執行
  • Looper:Looper 用於從 MessageQueue 中迴圈獲取 Message 並將之傳遞給訊息處理者(即訊息傳送者 Handler 本身)來進行消費,每條 Message 都有個 target 變數用來指向 Handler,以此把 Message 和其處理者關聯起來。不同執行緒之間通過互相拿到對方的 Looper 物件,以此來實現跨執行緒傳送訊息

有了以上的認知基礎後,下面就來看看實際的原始碼實現 ~ ~

二、Handler 原始碼

1、Handler 如何初始化

Handler 的建構函式一共有七個,除去兩個已經廢棄的和三個隱藏的,實際上開發者可以使用的只有兩個。而不管是使用哪個建構函式,最終的目的都是為了完成 mLooper、mQueue、mCallback、mAsynchronous 這四個常量的初始化,同時也可以看出來 MessageQueue 是由 Looper 來完成初始化的,而且 Handler 對於 Looper 和 MessageQueue 都是一對一的關係,一旦初始化後就不可改變

大部分開發者使用的應該都是 Handler 的無參建構函式,而在 Android 11 中 Handler 的無參建構函式已經被標記為廢棄的了。Google 官方更推薦的做法是通過顯式傳入 Looper 物件來完成初始化,而非隱式使用當前執行緒關聯的 Looper

Handler 對於 Looper 和 MessageQueue 都是一對一的關係,但是 Looper 和 MessageQueue 對於 Handler 可以是一對多的關係,這個後面會講到

	@UnsupportedAppUsage
    final Looper mLooper;
    final MessageQueue mQueue;
    @UnsupportedAppUsage
    final Callback mCallback;
    final boolean mAsynchronous;

	//省略其它建構函式

    /**
     * @hide
     */
    public Handler(@Nullable 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());
            }
        }

        mLooper = Looper.myLooper();
        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;
    }

2、Looper 如何初始化

在初始化 Handler 時,如果外部呼叫的建構函式沒有傳入 Looper,那就會呼叫Looper.myLooper()來獲取和當前執行緒關聯的 Looper 物件,再從 Looper 中取 MessageQueue。如果獲取到的 Looper 物件為 null 就會丟擲異常。根據異常資訊 Can't create handler inside thread that has not called Looper.prepare() 可以看出來,在初始化 Handler 之前需要先呼叫 Looper.prepare()完成 Looper 的初始化

走進 Looper 類中可以看到,myLooper()方法是 Looper 類的靜態方法,其只是單純地從 sThreadLocal 變數中取值並返回而已。sThreadLocal 又是通過 prepare(boolean) 方法來進行初始化賦值的,且只能賦值一次,重複呼叫將丟擲異常

我們知道,ThreadLocal 的特性就是可以為不同的執行緒分別維護單獨的一個變數例項,所以,不同的執行緒就會分別對應著不同的 Looper 物件,是一一對應的關係

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

    /**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

    /** 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 類的建構函式也是私有的,會初始化兩個常量值:mQueue 和 mThread,這說明了 Looper 對於 MessageQueue 和 Thread 都是一一對應的關係,關聯之後不能改變

	@UnsupportedAppUsage
    final MessageQueue mQueue;

	final Thread mThread;    

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

在日常開發中,我們在通過 Handler 來執行 UI 重新整理操作時,經常使用的是 Handler 的無參建構函式,那麼此時肯定就是使用了和主執行緒關聯的 Looper 物件,對應 Looper 類中的靜態變數 sMainLooper

    @UnsupportedAppUsage
    private static Looper sMainLooper;  // guarded by Looper.class

	//被標記為廢棄的原因是因為 sMainLooper 會交由 Android 系統自動來完成初始化,外部不應該主動來初始化
    @Deprecated
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

    /**
     * Returns the application's main looper, which lives in the main thread of the application.
     */
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

prepareMainLooper()就用於為主執行緒初始化 Looper 物件,該方法又是由 ActivityThread 類的 main() 方法來呼叫的。該 main() 方法即 Java 程式的執行起始點,所以當應用啟動時系統就自動為我們在主執行緒做好了 mainLooper 的初始化,而且已經呼叫了Looper.loop()方法開啟了訊息的迴圈處理,應用在使用過程中的各種互動邏輯(例如:螢幕的觸控事件、列表的滑動等)就都是在這個迴圈裡完成分發的

正是因為 Android 系統已經自動完成了主執行緒 Looper 的初始化,所以我們在主執行緒中才可以直接使用 Handler 的無參建構函式來完成 UI 相關事件的處理

public final class ActivityThread extends ClientTransactionHandler {
 
    public static void main(String[] args) {
        ···
        Looper.prepareMainLooper();
        ···
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    
}

3、Handler 傳送訊息

Handler 用於傳送訊息的方法非常多,有十幾個,其中大部分最終呼叫到的都是 sendMessageAtTime() 方法。uptimeMillis 即 Message 具體要執行的時間戳,如果該時間戳比當前時間大,那麼就意味著要執行的是延遲任務。如果為 mQueue 為 null,就會列印異常資訊並直接返回,因為 Message 是需要交由 MessageQueue 來處理的

 	public boolean sendMessageAtTime(@NonNull 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);
    }

需要注意 msg.target = this 這句程式碼,target 指向了傳送訊息的主體,即 Handler 物件本身,即由 Handler 物件發給 MessageQueue 的訊息最後還是要交由 Handler 物件本身來處理

	private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        //將訊息交由 MessageQueue 處理
        return queue.enqueueMessage(msg, uptimeMillis);
    }

4、MessageQueue

MessageQueue 通過 enqueueMessage 方法來接收訊息

  • 因為存在多個執行緒同時往一個 MessageQueue 傳送訊息的可能,所以 enqueueMessage 內部肯定需要進行執行緒同步
  • 可以看出 MessageQueue 內部是以連結串列的結構來儲存 Message 的(Message.next),根據 Message 的時間戳大小來決定其在訊息佇列中的位置
  • mMessages 代表的是訊息佇列中的第一條訊息。如果 mMessages 為空(訊息佇列是空的),或者 mMessages 的時間戳要比新訊息的時間戳大,則將新訊息插入到訊息佇列的頭部;如果 mMessages 不為空,則尋找訊息列隊中第一條觸發時間比新訊息晚的非空訊息,將新訊息插到該訊息的前面

到此,一個按照時間戳大小進行排序的訊息佇列就完成了,後邊要做的就是從訊息佇列中依次取出訊息進行處理了

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        synchronized (this) {
            ···
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            //用於標記是否需要喚醒執行緒
            boolean needWake;
            //如果連結串列是空的,或者處於隊頭的訊息的時間戳比 msg 要大,則將 msg 作為連結串列頭部
            //when == 0 說明 Handler 呼叫的是 sendMessageAtFrontOfQueue 方法,直接將 msg 插到佇列頭部 
            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 {
                //如果當前執行緒處於休眠狀態 + 隊頭訊息是屏障訊息 + msg 是非同步訊息
                //那麼就需要喚醒執行緒
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                
                Message prev;
                //從連結串列頭向連結串列尾遍歷,尋找連結串列中第一條時間戳比 msg 大的訊息,將 msg 插到該訊息的前面
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        //如果在 msg 之前佇列中還有非同步訊息那麼就不需要主動喚醒
                        //因為已經設定喚醒時間了
                        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;
    }

知道了 Message 是如何儲存的了,再來看下 MessageQueue 是如何取出 Message 並回撥給 Handler 的。在 MessageQueue 中讀取訊息的操作對應的是next() 方法。next() 方法內部開啟了一個無限迴圈,如果訊息佇列中沒有訊息或者是隊頭訊息還沒到可以處理的時間,該方法就會導致 Loop 執行緒休眠掛起,直到條件滿足後再重新遍歷訊息

	@UnsupportedAppUsage
    Message next() {
        ···
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            //將 Loop 執行緒休眠掛起
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                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;
                }
                ···
            }
            ···
            }
			···
        }
    }

next() 方法又是通過 Looper 類的 loop() 方法來迴圈呼叫的,loop() 方法內也是一個無限迴圈,唯一跳出迴圈的條件就是 queue.next()方法返回為 null。因為 next() 方法可能會觸發阻塞操作,所以沒有訊息需要處理時也會導致 loop() 方法被阻塞著,而當 MessageQueue 有了新的訊息,Looper 就會及時地處理這條訊息並呼叫 msg.target.dispatchMessage(msg) 方法將訊息回傳給 Handler 進行處理

	/**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        ···	
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            ···
            msg.target.dispatchMessage(msg);
            ···
        }
    }

Handler 的dispatchMessage方法就是在向外部分發 Message 了。至此,Message 的整個分發流程就結束了

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

5、訊息屏障

Android 系統為了保證某些高優先順序的 Message(非同步訊息) 能夠被儘快執行,採用了一種訊息屏障(Barrier)機制。其大致流程是:先傳送一個屏障訊息到 MessageQueue 中,當 MessageQueue 遍歷到該屏障訊息時,就會判斷當前佇列中是否存在非同步訊息,有的話則先跳過同步訊息(開發者主動傳送的都屬於同步訊息),優先執行非同步訊息。這種機制就會使得在非同步訊息被執行完之前,同步訊息都不會得到處理

Handler 的建構函式中的async引數就用於控制傳送的 Message 是否屬於非同步訊息

    public class Handler {

		final boolean mAsynchronous;

		public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
        	···
        	mAsynchronous = async;
    	}

    	private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            	long uptimeMillis) {
        	msg.target = this;
        	msg.workSourceUid = ThreadLocalWorkSource.getUid();
        	if (mAsynchronous) {
                //設為非同步訊息
            	msg.setAsynchronous(true);
        	}
        	return queue.enqueueMessage(msg, uptimeMillis);
    	}
       
    }

MessageQueue 在取隊頭訊息的時候,如果判斷到隊頭訊息就是屏障訊息的話,那麼就會向後遍歷找到第一條非同步訊息優先進行處理

	@UnsupportedAppUsage
    Message next() {
        ···
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) { //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());
                }
                ···
            }
            ···
        }
    }

6、退出 Looper 迴圈

Looper 類本身做了方法限制,除了主執行緒外,子執行緒關聯的 MessageQueue 都支援退出 Loop 迴圈,即 quitAllowed 只有主執行緒才能是 false

public final class Looper {
    
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
    
    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));
    }
    
}

MessageQueue 支援兩種方式來退出 Loop:

  • safe 為 true,只移除所有尚未執行的訊息,不移除時間戳等於當前時間的訊息
  • safe 為 false,移除所有訊息
	void quit(boolean safe) {
        if (!mQuitAllowed) {
            //MessageQueue 設定了不允許退出迴圈,直接丟擲異常
            throw new IllegalStateException("Main thread not allowed to quit.");
        }
        synchronized (this) {
            if (mQuitting) {
                //避免重複呼叫
                return;
            }
            mQuitting = true;
            if (safe) {
                //只移除所有尚未執行的訊息,不移除時間戳等於當前時間的訊息
                removeAllFutureMessagesLocked();
            } else {
                //移除所有訊息
                removeAllMessagesLocked();
            }
            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
    }

7、IdleHandler

IdleHandler 是 MessageQueue 的一個內部介面,可以用於在 Loop 執行緒處於空閒狀態的時候執行一些優先順序不高的操作

    public static interface IdleHandler {
        boolean queueIdle();
    }

MessageQueue 在獲取隊頭訊息時,如果發現當前沒有需要執行的 Message 的話,那麼就會去遍歷 mIdleHandlers,依次執行 IdleHandler

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

	@UnsupportedAppUsage
    Message next() {
        ···
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            ···
            synchronized (this) {
                ···
                //如果隊頭訊息 mMessages 為 null 或者 mMessages 需要延遲處理
                //那麼就來執行 IdleHandler
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                boolean keep = false;
                try {
                    //執行 IdleHandler
                    //如果返回 false 的話說明之後不再需要執行,那就將其移除
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
			···
        }
    }

例如,ActivityThread 就向主執行緒 MessageQueue 新增了一個 GcIdler,用於在主執行緒空閒時嘗試去執行 GC 操作

public final class ActivityThread extends ClientTransactionHandler {
    
    @UnsupportedAppUsage
    void scheduleGcIdler() {
        if (!mGcIdlerScheduled) {
            mGcIdlerScheduled = true;
            //新增 IdleHandler
            Looper.myQueue().addIdleHandler(mGcIdler);
        }
        mH.removeMessages(H.GC_WHEN_IDLE);
    }
    
    final class GcIdler implements MessageQueue.IdleHandler {
        @Override
        public final boolean queueIdle() {
            //嘗試 GC
            doGcIfNeeded();
            purgePendingResources();
            return false;
        }
    }
    
}

8、做一個總結

再來總結下以上的所有內容

  1. 每個 Handler 都會和一個 Looper 例項關聯在一起,可以在初始化 Handler 時通過建構函式主動傳入例項,否則就會預設使用和當前執行緒關聯的 Looper 物件
  2. 每個 Looper 都會和一個 MessageQueue 例項關聯在一起,每個執行緒都需要通過呼叫 Looper.prepare()方法來初始化本執行緒獨有的 Looper 例項,並通過呼叫Looper.loop()方法來使得本執行緒迴圈向 MessageQueue 取出訊息並執行。Android 系統預設會為每個應用初始化和主執行緒關聯的 Looper 物件,並且預設就開啟了 loop 迴圈來處理主執行緒訊息
  3. MessageQueue 按照連結結構來儲存 Message,執行時間早(即時間戳小)的 Message 會排在連結串列的頭部,Looper 會迴圈從連結串列中取出 Message 並回撥給 Handler,取值的過程可能會包含阻塞操作
  4. Message、Handler、Looper、MessageQueue 這四者就構成了一個生產者和消費者模式。Message 相當於產品,MessageQueue 相當於傳輸管道,Handler 相當於生產者,Looper 相當於消費者
  5. Handler 對於 Looper、Handler 對於 MessageQueue、Looper 對於 MessageQueue、Looper 對於 Thread ,這幾個之間都是一一對應的關係,在關聯後無法更改,但 Looper 對於 Handler、MessageQueue 對於 Handler 可以是一對多的關係
  6. Handler 能用於更新 UI 包含了一個隱性的前提條件:Handler 與主執行緒 Looper 關聯在了一起。在主執行緒中初始化的 Handler 會預設與主執行緒 Looper 關聯在一起,所以其 handleMessage(Message msg) 方法就會由主執行緒來呼叫。在子執行緒初始化的 Handler 如果也想執行 UI 更新操作的話,則需要主動獲取 mainLooper 來初始化 Handler
  7. 對於我們自己在子執行緒中建立的 Looper,當不再需要的時候我們應該主動退出迴圈,否則子執行緒將一直無法得到釋放。對於主執行緒 Loop 我們則不應該去主動退出,否則將導致應用崩潰
  8. 我們可以通過向 MessageQueue 新增 IdleHandler 的方式,來實現在 Loop 執行緒處於空閒狀態的時候執行一些優先順序不高的任務。例如,假設我們有個需求是希望當主執行緒完成介面繪製等事件後再執行一些 UI 操作,那麼就可以通過 IdleHandler 來實現,這可以避免拖慢使用者看到首屏頁面的速度

三、Handler 在系統中的應用

1、HandlerThread

HandlerThread 是 Android SDK 中和 Handler 在同個包下的一個類,從其名字就可以看出來它是一個執行緒,而且使用到了 Handler

其用法類似於以下程式碼。通過 HandlerThread 內部的 Looper 物件來初始化 Handler,同時在 Handler 中宣告需要執行的耗時任務,主執行緒通過向 Handler 傳送訊息來觸發 HandlerThread 去執行耗時任務

class MainActivity : AppCompatActivity() {

    private val handlerThread = HandlerThread("I am HandlerThread")

    private val handler by lazy {
        object : Handler(handlerThread.looper) {
            override fun handleMessage(msg: Message) {
                Thread.sleep(2000)
                Log.e("MainActivity", "這裡是子執行緒,可以用來執行耗時任務:" + Thread.currentThread().name)
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btn_test.setOnClickListener {
            handler.sendEmptyMessage(1)
        }
        handlerThread.start()
    }

}

HandlerThread 的原始碼還是挺簡單的,只有一百多行

HandlerThread 是 Thread 的子類,其作用就是為了用來執行耗時任務,其 run()方法會自動為自己建立一個 Looper 物件並儲存到 mLooper,之後就主動開啟訊息迴圈,這樣 HandlerThread 就會來迴圈處理 Message 了

public class HandlerThread extends Thread {
    
    //執行緒優先順序
    int mPriority;
    //執行緒ID
    int mTid = -1;
    //當前執行緒持有的 Looper 物件
    Looper mLooper;
    
    private @Nullable Handler mHandler;

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }

    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }
    
    @Override
    public void run() {
        mTid = Process.myTid();
        //觸發當前執行緒建立 Looper 物件
        Looper.prepare();
        synchronized (this) {
            //獲取 Looper 物件
            mLooper = Looper.myLooper();
            //喚醒所有處於等待狀態的執行緒
            notifyAll();
        }
        //設定執行緒優先順序
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        //開啟訊息迴圈
        Looper.loop();
        mTid = -1;
    }
    
}

此外,HandlerThread 還包含一個getLooper()方法用於獲取 Looper。當我們在外部呼叫handlerThread.start()啟動執行緒後,由於其run()方法的執行時機依然是不確定的,所以 getLooper()方法就必須等到 Looper 初始化完畢後才能返回,否則就會由於wait()方法而一直阻塞等待。當run()方法初始化 Looper 完成後,就會呼叫notifyAll()來喚醒所有處於等待狀態的執行緒。所以外部在使用 HandlerThread 前就記得必須先呼叫 start() 方法來啟動 HandlerThread

    //獲取與 HandlerThread 關聯的 Looper 物件
    //因為 getLooper() 可能先於 run() 被執行
	//所以當 mLooper 為 null 時呼叫者執行緒就需要阻塞等待 Looper 物件建立完畢
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

HandlerThread 起到的作用就是方便了主執行緒和子執行緒之間的互動,主執行緒可以直接通過 Handler 來宣告耗時任務並交由子執行緒來執行。使用 HandlerThread 也方便在多個執行緒間共享,主執行緒和其它子執行緒都可以向 HandlerThread 下發任務,且 HandlerThread 可以保證多個任務執行時的有序性

2、IntentService

IntentService 是系統提供的 Service 子類,用於在後臺序列執行耗時任務,在處理完所有任務後會自動停止,不必來手動呼叫 stopSelf() 方法。而且由於IntentService 是四大元件之一,擁有較高的優先順序,不易被系統殺死,因此適合用於執行一些高優先順序的非同步任務

Google 官方以前也推薦開發者使用 IntentService,但是在 Android 11 中已經被標記為廢棄狀態了,但這也不妨礙我們來了解下其實現原理

IntentService 內部依靠 HandlerThread 來實現,其 onCreate()方法會建立一個 HandlerThread,拿到 Looper 物件來初始化 ServiceHandler。ServiceHandler 會將其接受到的每個 Message 都轉交由抽象方法 onHandleIntent來處理,子類就通過實現該方法來宣告耗時任務

public abstract class IntentService extends Service {
    
    private volatile Looper mServiceLooper;
    @UnsupportedAppUsage
    private volatile ServiceHandler mServiceHandler;
    
    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }
    
    @Override
    public void onCreate() {
        super.onCreate();
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        //觸發 HandlerThread 建立 Looper 物件
        thread.start();
		//獲取 Looper 物件,構建可以向 HandlerThread 傳送 Message 的 Handler
        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }
    
    @WorkerThread
    protected abstract void onHandleIntent(@Nullable Intent intent);
    
}

每次 start IntentService 時,onStart()方法就會被呼叫,將 intentstartId 包裝為一個 Message 物件後傳送給mServiceHandler。需要特別注意的是 startId 這個引數,它用於唯一標識每次對 IntentService 發起的任務請求,每次回撥 onStart() 方法時,startId 的值都是自動遞增的。IntentService 不應該在處理完一個 Message 之後就立即停止 IntentService,因為此時 MessageQueue 中可能還有待處理的任務還未取出來,所以如果當呼叫 stopSelf(int)方法時傳入的引數不等於當前最新的 startId 值的話,那麼stopSelf(int) 方法就不會導致 IntentService 被停止,從而避免了將尚未處理的 Message 給遺漏了

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    }

    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        onStart(intent, startId);
        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
    }

四、Handler 在三方庫中的應用

1、EventBus

EventBus 的 Github 上有這麼一句介紹:EventBus is a publish/subscribe event bus for Android and Java. 這說明了 EventBus 是普遍適用於 Java 環境的,只是對 Android 系統做了特殊的平臺支援而已。EventBus 的四種訊息傳送策略包含了ThreadMode.MAIN 用於指定在主執行緒進行訊息回撥,其內部就是通過 Handler 來實現的

EventBusBuilder 會去嘗試獲取 MainLooper,如果拿得到的話就可以用來初始化 HandlerPoster,從而實現主執行緒回撥

	MainThreadSupport getMainThreadSupport() {
        if (mainThreadSupport != null) {
            return mainThreadSupport;
        } else if (AndroidLogger.isAndroidLogAvailable()) {
            Object looperOrNull = getAndroidMainLooperOrNull();
            return looperOrNull == null ? null :
                    new MainThreadSupport.AndroidHandlerMainThreadSupport((Looper) looperOrNull);
        } else {
            return null;
        }
    }

    static Object getAndroidMainLooperOrNull() {
        try {
            return Looper.getMainLooper();
        } catch (RuntimeException e) {
            // Not really a functional Android (e.g. "Stub!" maven dependencies)
            return null;
        }
    }
public class HandlerPoster extends Handler implements Poster {

    protected HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {
        super(looper);
        ···
    }
    
	···
        
    @Override
    public void handleMessage(Message msg) {
        ···
    }
}

2、Retrofit

和 EventBus 一樣,Retrofit 的內部實現也不需要依賴於 Android 平臺,而是可以用於任意的 Java 客戶端,Retrofit 只是對 Android 平臺進行了特殊實現而已

在構建 Retrofit 物件的時候,我們可以選擇傳遞一個 Platform 物件用於標記呼叫方所處的平臺

public static final class Builder {
    
    private final Platform platform;
    
    Builder(Platform platform) {
      this.platform = platform;
    }

	···
}

Platform 類只具有一個唯一子類,即 Android 類。其主要邏輯就是重寫了父類的 defaultCallbackExecutor()方法,通過 Handler 來實現在主執行緒回撥網路請求結果

static final class Android extends Platform {
    
    @Override
    public Executor defaultCallbackExecutor() {
      return new MainThreadExecutor();
    }

    ···

    static final class MainThreadExecutor implements Executor {
      private final Handler handler = new Handler(Looper.getMainLooper());

      @Override
      public void execute(Runnable r) {
        handler.post(r);
      }
    }
  }

五、面試環節

1、Handler、Looper、MessageQueue、Thread 的對應關係

首先,Looper 中的 MessageQueue 和 Thread 兩個欄位都屬於常量,且 Looper 例項是存在 ThreadLocal 中,這說明了 Looper 和 MessageQueue 之間是一對一應的關係,且一個 Thread 在其整個生命週期內都只會關聯到同一個 Looper 物件和同一個 MessageQueue 物件

public final class Looper {
 
   final MessageQueue mQueue;
   final Thread mThread;
   static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    
   private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
    
}

Handler 中的 Looper 和 MessageQueue 兩個欄位也都屬於常量,說明 Handler 對於 Looper 和 MessageQueue 都是一對一的關係。但是 Looper 和 MessageQueue 對於 Handler 卻可以是一對多的關係,例如,多個子執行緒內宣告的 Handler 都可以關聯到 mainLooper

public class Handler {
    
    @UnsupportedAppUsage
    final Looper mLooper;
    final MessageQueue mQueue;
    
}

2、Handler 的同步機制

MessageQueue 在儲存 Message 的時候,enqueueMessage方法內部已經加上了同步鎖,從而避免了多個執行緒同時傳送訊息導致競態問題。此外,next()方法內部也加上了同步鎖,所以也保障了 Looper 分發 Message 的有序性。最重要的一點是,Looper 總是由一個特定的執行緒來執行遍歷,所以在消費 Message 的時候也不存在競態

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        synchronized (this) {
            ···
        }
        return true;
    }


    @UnsupportedAppUsage
    Message next() {
        ···
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                ···
            }

            ···
        }
    }

3、Handler 如何傳送同步訊息

如果我們在子執行緒通過 Handler 向主執行緒傳送了一個訊息,希望等到訊息執行完畢後子執行緒才繼續執行,這該如何實現?其實像這種涉及到多執行緒同步等待的問題,往往都是需要依賴於執行緒休眠+執行緒喚醒機制來實現的

Handler 本身就提供了一個runWithScissors方法可以用於實現這種功能,只是被隱藏了,我們無法直接呼叫到。runWithScissors首先會判斷目標執行緒是否就是當前執行緒,是的話則直接執行 Runnable,否則就需要使用到 BlockingRunnable

    /**
     * @hide
     */
    public final boolean runWithScissors(@NonNull Runnable r, long timeout) {
        if (r == null) {
            throw new IllegalArgumentException("runnable must not be null");
        }
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout must be non-negative");
        }

        if (Looper.myLooper() == mLooper) {
            r.run();
            return true;
        }

        BlockingRunnable br = new BlockingRunnable(r);
        return br.postAndWait(this, timeout);
    }

BlockingRunnable 的邏輯也很簡單,在 Runnable 執行完前會通過呼叫 wait()方法來使傳送者執行緒轉為阻塞等待狀態,當任務執行完畢後再通過notifyAll()來喚醒傳送者執行緒,從而實現了在 Runnable 被執行完之前傳送者執行緒都會一直處於等待狀態

private static final class BlockingRunnable implements Runnable {
    
        private final Runnable mTask;
    	//用於標記 mTask 是否已經執行完畢 
        private boolean mDone;

        public BlockingRunnable(Runnable task) {
            mTask = task;
        }

        @Override
        public void run() {
            try {
                mTask.run();
            } finally {
                synchronized (this) {
                    mDone = true;
                    notifyAll();
                }
            }
        }

        public boolean postAndWait(Handler handler, long timeout) {
            if (!handler.post(this)) {
                return false;
            }

            synchronized (this) {
                if (timeout > 0) {
                    final long expirationTime = SystemClock.uptimeMillis() + timeout;
                    while (!mDone) {
                        long delay = expirationTime - SystemClock.uptimeMillis();
                        if (delay <= 0) {
                            return false; // timeout
                        }
                        try {
                            //限時等待
                            wait(delay);
                        } catch (InterruptedException ex) {
                        }
                    }
                } else {
                    while (!mDone) {
                        try {
                            //無限期等待
                            wait();
                        } catch (InterruptedException ex) {
                        }
                    }
                }
            }
            return true;
        }
    }

雖然 runWithScissors 方法我們無法直接呼叫,但是我們也可以依靠這思路自己來實現 BlockingRunnable,折中實現這個功能。但這種方式並不安全,如果 Loop 意外退出迴圈導致該 Runnable 無法被執行的話,就會導致被暫停的執行緒一直無法被喚醒,需要謹慎使用

4、Handler 如何避免記憶體洩漏

當退出 Activity 時,如果 Handler 中還儲存著待處理的延時訊息的話,那麼就會導致記憶體洩漏,此時可以通過呼叫Handler.removeCallbacksAndMessages(null)來移除所有待處理的 Message

該方法會將訊息佇列中所有 Message.obj 等於 token 的 Message 均給移除掉,如果 token 為 null 的話則會移除所有 Message

    public final void removeCallbacksAndMessages(@Nullable Object token) {
        mQueue.removeCallbacksAndMessages(this, token);
    }

5、Message 如何複用

因為 Android 系統本身就存在很多事件需要交由 Message 來交付給 mainLooper,所以 Message 的建立是很頻繁的。為了減少 Message 頻繁重複建立的情況,Message 提供了 MessagePool 用於實現 Message 的快取複用,以此來優化記憶體使用

當 Looper 消費了 Message 後會呼叫recycleUnchecked()方法將 Message 進行回收,在清除了各項資源後會快取到 sPool 變數上,同時將之前快取的 Message 置為下一個節點 next,通過這種連結串列結構來快取最多 50 個Message。這裡使用到的是享元設計模式

obtain()方法則會判斷當前是否有可用的快取,有的話則將 sPool 從連結串列中移除後返回,否則就返回一個新的 Message 例項。所以我們在傳送訊息的時候應該儘量通過呼叫Message.obtain()或者Handler.obtainMessage()方法來獲取 Message 例項

public final class Message implements Parcelable {
    
    /** @hide */
    public static final Object sPoolSync = new Object();
    private static Message sPool;
    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 50;
    
    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();
    }
    
    @UnsupportedAppUsage
    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++;
            }
        }
    }
    
}

6、Message 複用機制存在的問題

由於 Message 採用了快取複用機制,從而導致了一個 Message 失效問題。當 handleMessage 方法被回撥後,Message 攜帶的所有引數都會被清空,而如果外部的 handleMessage方法是使用了非同步執行緒來處理 Message 的話,那麼非同步執行緒只會得到一個空白的 Message

val handler = object : Handler() {
    override fun handleMessage(msg: Message) {
        handleMessageAsync(msg)
    }
}

fun handleMessageAsync(msg: Message) {
    thread {
        //只會得到一個空白的 Message 物件
        println(msg.obj)
    }
}

7、Message 如何提高優先順序

Handler 包含一個 sendMessageAtFrontOfQueue方法可以用於提高 Message 的處理優先順序。該方法為 Message 設定的時間戳是 0,使得 Message 可以直接插入到 MessageQueue 的頭部,從而做到優先處理。但官方並不推薦使用這個方法,因為最極端的情況下可能會使得其它 Message 一直得不到處理或者其它意想不到的情況

    public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg) {
        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, 0);
    }

8、檢測 Looper 分發 Message 的效率

Looper 在進行 Loop 迴圈時,會通過 Observer 向外回撥每個 Message 的回撥事件。且如果設定了 slowDispatchThresholdMsslowDeliveryThresholdMs 這兩個閾值的話,則會對 Message 的分發時機分發耗時進行監測,存在異常情況的話就會列印 Log。該機制可以用於實現應用效能監測,發現潛在的 Message 處理異常情況,但可惜監測方法被系統隱藏了

	public static void loop() {
        final Looper me = myLooper();
        ···
        for (;;) {
            Message msg = queue.next(); // might block
			···
            //用於向外回撥通知 Message 的分發事件
            final Observer observer = sObserver;

            final long traceTag = me.mTraceTag;
            //如果Looper分發Message的時間晚於預定時間且超出這個閾值,則認為Looper分發過慢
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            //如果向外分發出去的Message的處理時間超出這個閾值,則認為外部處理過慢
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            //開始分發 Message 的時間
            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            //Message 分發結束的時間
            final long dispatchEnd;
            Object token = null;
            if (observer != null) {
                //開始分發 Message 
                token = observer.messageDispatchStarting();
            }
            long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
            try {
                msg.target.dispatchMessage(msg);
                if (observer != null) {
                    //完成 Message 的分發,且沒有丟擲異常
                    observer.messageDispatched(token, msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } catch (Exception exception) {
                if (observer != null) {
                    //分發 Message 時丟擲了異常
                    observer.dispatchingThrewException(token, msg, exception);
                }
                throw exception;
            } finally {
                ThreadLocalWorkSource.restore(origWorkSource);
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        //如果 Message 的分發時間晚於預定時間,且間隔超出10毫秒,則認為屬於延遲交付
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            ···
        }
    }

9、主執行緒 Looper 在哪裡建立

由 ActivityThread 類的 main() 方法來建立。該 main() 方法即 Java 程式的執行起始點,當應用啟動時系統就自動為我們在主執行緒做好了 mainLooper 的初始化,而且已經呼叫了Looper.loop()方法開啟了訊息的迴圈處理,應用在使用過程中的各種互動邏輯(例如:螢幕的觸控事件、列表的滑動等)就都是在這個迴圈裡完成分發的。正是因為 Android 系統已經自動完成了主執行緒 Looper 的初始化,所以我們在主執行緒中才可以直接使用 Handler 的無參建構函式來完成 UI 相關事件的處理

public final class ActivityThread extends ClientTransactionHandler {
 
    public static void main(String[] args) {
        ···
        Looper.prepareMainLooper();
        ···
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    
}

10、主執行緒 Looper 什麼時候退出迴圈

當 ActivityThread 內部的 Handler 收到了 EXIT_APPLICATION 訊息後,就會退出 Looper 迴圈

		public void handleMessage(Message msg) {
            switch (msg.what) {
                case EXIT_APPLICATION:
                    if (mInitialApplication != null) {
                        mInitialApplication.onTerminate();
                    }
                    Looper.myLooper().quit();
                    break;
            }
		}

11、主執行緒 Looper.loop() 為什麼不會導致 ANR

這個問題在網上很常見,我第一次看到時就覺得這種問題很奇怪,主執行緒憑啥會 ANR?這個問題感覺本身就是特意為了來誤導人

看以下例子。doSomeThing()方法是放在 for 迴圈這個死迴圈的後邊,對於該方法來說,主執行緒的確是被阻塞住了,導致該方法一直無法得到執行。可是對於應用來說,應用在主執行緒內的所有操作其實都是被放在了 for 迴圈之內,一直有得到執行,是個死迴圈也無所謂,所以對於應用來說主執行緒並沒有被阻塞,自然不會導致 ANR。此外,當 MessageQueue 中當前沒有訊息需要處理時,也會依靠 epoll 機制掛起主執行緒,避免了其一直佔用 CPU 資源

    public static void main(String[] args) {
        for (; ; ) {
            //主執行緒執行....
        }
        doSomeThing();
    }

所以在 ActivityThread 的 main 方法中,在開啟了訊息迴圈之後,並沒有宣告什麼有意義的程式碼。正常來說應用是不會退出 loop 迴圈的,如果能夠跳出迴圈,也只會導致直接就丟擲異常

	public static void main(String[] args) {
        ···
        Looper.prepareMainLooper();
        ···
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

所以說,loop 迴圈本身不會導致 ANR,會出現 ANR 是因為在 loop 迴圈之內 Message 處理時間過長

12、子執行緒一定無法彈 Toast 嗎

不一定,只能說是在子執行緒中無法直接彈出 Toast,但可以實現。因為 Toast 的建構函式中會要求拿到一個 Looper 物件,如果構造引數沒有傳入不為 null 的 Looper 例項的話,則嘗試使用呼叫者執行緒關聯的 Looper 物件,如果都獲取不到的話則會丟擲異常

    public Toast(Context context) {
        this(context, null);
    }

	public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mToken = new Binder();
        looper = getLooper(looper);
        mHandler = new Handler(looper);
        ···
    }

    private Looper getLooper(@Nullable Looper looper) {
        if (looper != null) {
            return looper;
        }
        //Looper.myLooper() 為 null 的話就會直接丟擲異常
        return checkNotNull(Looper.myLooper(),
                "Can't toast on a thread that has not called Looper.prepare()");
    }

為了在子執行緒彈 Toast,就需要主動為子執行緒建立 Looper 物件及開啟 loop 迴圈。但這種方法會導致子執行緒一直無法退出迴圈,需要通過Looper.myLooper().quit()來主動退出迴圈

    inner class TestThread : Thread() {

        override fun run() {
            Looper.prepare()
            Toast.makeText(
                this@MainActivity,
                "Hello: " + Thread.currentThread().name,
                Toast.LENGTH_SHORT
            ).show()
            Looper.loop()
        }

    }

13、子執行緒一定無法更新 UI?主執行緒就一定可以?

在子執行緒能夠彈出 Toast 就已經說明了子執行緒也是可以更新 UI 的,Android 系統只是限制了必須在同個執行緒內進行 ViewRootImpl 的建立和更新這兩個操作,而不是要求必須在主執行緒進行

如果使用不當的話,即使在主執行緒更新 UI 也可能會導致應用崩潰。例如,在子執行緒先通過 show+hide 來觸發 ViewRootImpl 的建立,然後在主執行緒再來嘗試顯示該 Dialog,此時就會發現程式直接崩潰了

class MainActivity : AppCompatActivity() {

    private lateinit var alertDialog: AlertDialog

    private val thread = object : Thread("hello") {
        override fun run() {
            Looper.prepare()
            Handler().post {
                alertDialog =
                    AlertDialog.Builder(this@MainActivity).setMessage(Thread.currentThread().name)
                        .create()
                alertDialog.show()
                alertDialog.hide()
            }
            Looper.loop()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btn_test.setOnClickListener {
            alertDialog.show()
        }
        thread.start()
    }

}
    E/AndroidRuntime: FATAL EXCEPTION: main
    Process: github.leavesc.test, PID: 5243
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6892)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1048)
        at android.view.View.requestLayout(View.java:19781)
        at android.view.View.setFlags(View.java:11478)
        at android.view.View.setVisibility(View.java:8069)
        at android.app.Dialog.show(Dialog.java:293)

ViewRootImpl 在初始化的時候會將當前執行緒儲存到 mThread,在後續進行 UI 更新的時候就會呼叫checkThread()方法進行執行緒檢查,如果發現存在多執行緒呼叫則直接丟擲以上的異常資訊

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
            
     final Thread mThread;       
            
     public ViewRootImpl(Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
        mThread = Thread.currentThread();
        ···
    }       
            
    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
            
}

14、為什麼 UI 體系要採用單執行緒模型

其實這很好理解,就是為了提高執行效率和降低實現難度。如果允許多執行緒併發訪問 UI 的話,為了避免競態,很多即使只是小範圍的區域性重新整理操作(例如,TextView.setText)都勢必需要加上同步鎖,這無疑會加大 UI 重新整理操作的“成本”,降低了整個應用的執行效率。而且會導致 Android 的 UI 體系在實現時就被迫需要對多執行緒環境進行“防禦”,即使開發者一直是使用同個執行緒來更新 UI,這就加大了系統的實現難度

所以,最為簡單高效的方式就是採用單執行緒模型來訪問 UI

15、如何跨執行緒下發任務

通常情況下,兩個執行緒之間的通訊是比較麻煩的,需要做很多執行緒同步操作。而依靠 Looper 的特性,我們就可以用比較簡單的方式來實現跨執行緒下發任務

看以下程式碼,從 TestThread 執行後彈出的執行緒名可以知道, Toast 是在 Thread_1 被彈出來的。如果將 Thread_2 想像成主執行緒的話,那麼以下程式碼就相當於從主執行緒向子執行緒下發耗時任務了,這個實現思路就相當於 Android 提供的 HandlerThread 類

	inner class TestThread : Thread("Thread_1") {

        override fun run() {
            Looper.prepare()
            val looper = Looper.myLooper()
            object : Thread("Thread_2") {
                override fun run() {
                    val handler = Handler(looper!!)
                    handler.post {
                        //輸出結果是:Thread_1
                        Toast.makeText(
                            this@MainActivity,
                            Thread.currentThread().name,
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
            }.start()
            Looper.loop()
        }

    }

16、如何判斷當前是不是主執行緒

通過 Looper 來判斷

        if (Looper.myLooper() == Looper.getMainLooper()) {
            //是主執行緒
        }

        if (Looper.getMainLooper().isCurrentThread){
            //是主執行緒
        }

17、如何全域性捕獲主執行緒異常

比較臥槽的一個做法就是通過巢狀 Loop 迴圈來實現。向主執行緒 Loop 傳送 一個 Runnable,在 Runnable 裡死迴圈執行 Loop 迴圈,這就會使得主執行緒訊息佇列中的所有任務都會被交由該 Runnable 來呼叫,只要加上 try catch 後就可以捕獲主執行緒的任意異常了,做到主執行緒永不崩潰

        Handler(Looper.getMainLooper()).post {
            while (true) {
                try {
                    Looper.loop()
                } catch (throwable: Throwable) {
                    throwable.printStackTrace()
                    Log.e("TAG", throwable.message ?: "")
                }
            }
        }

六、文章推薦

三方庫原始碼筆記(1)-EventBus 原始碼詳解

三方庫原始碼筆記(2)-EventBus 自己實現一個?

三方庫原始碼筆記(3)-ARouter 原始碼詳解

三方庫原始碼筆記(4)-ARouter 自己實現一個?

三方庫原始碼筆記(5)-LeakCanary 原始碼詳解

三方庫原始碼筆記(6)-LeakCanary 擴充套件閱讀

三方庫原始碼筆記(7)-超詳細的 Retrofit 原始碼解析

三方庫原始碼筆記(8)-Retrofit 與 LiveData 的結合使用

三方庫原始碼筆記(9)-超詳細的 Glide 原始碼詳解

三方庫原始碼筆記(10)-Glide 你可能不知道的知識點

三方庫原始碼筆記(11)-OkHttp 原始碼詳解

三方庫原始碼筆記(12)-OkHttp / Retrofit 開發除錯利器

三方庫原始碼筆記(13)-可能是全網第一篇 Coil 的原始碼分析文章

相關文章