Handler機制實現原理總結

2023-06-14 21:00:44

Handler一般用於執行緒間通訊,如常用的子執行緒使用handler讓主執行緒更新UI。那麼這是怎麼實現的呢?
我們先把這個大問題分解成多個小問題:

  1. post();postDelayed();sendMessage();sendEmptyMessage();等方法有什麼不同?
  2. Handler為什麼需要一個Looper,為什麼它不能為空?
  3. Handler為什麼可以做到執行緒間通訊?
  4. postDelayed()為什麼可以讓執行緒延遲執行?

接下來帶著這些疑惑去尋找答案。

post();postDelayed();sendMessage();sendEmptyMessage();等方法有什麼不同?

它們最終都是呼叫同一個方法:sendMessageAtTime(),只是引數不同,Handler幫我們進行了一下封裝。

先看post();postDelayed();這兩個方法,檢視原始碼可以發現,這兩個方法都是呼叫sendMessageDelayed(Message, long)。只是post()的時間這個引數是0。
程式碼如下:

public final boolean post(@NonNull Runnable r) {  
   return  sendMessageDelayed(getPostMessage(r), 0);  
}
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {  
    return sendMessageDelayed(getPostMessage(r), delayMillis);  
}
private static Message getPostMessage(Runnable r) {  
    Message m = Message.obtain();  
    m.callback = r;  
    return m;  
}

關鍵是將Runable任務封裝成Message的這個getPostMessage()
這裡並不是簡單地將Runable封裝成Message,這裡還有一個Message回收池機制的實現。將在下文展開介紹。

再看sendMessage();sendEmptyMessage();這兩個方法:

public final boolean sendMessage(@NonNull Message msg) {  
    return sendMessageDelayed(msg, 0);  
}
public final boolean sendEmptyMessage(int what){  
    return sendEmptyMessageDelayed(what, 0);  
}
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {  
    Message msg = Message.obtain();  
    msg.what = what;  
    return sendMessageDelayed(msg, delayMillis);  
}

可以看到這四個方法最終都是呼叫sendMessageDelayed():

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

上面sendMessageDelayed()的實現都很簡單,但需要注意的是這裡使用了SystemClock.uptimeMillis(),它返回的是裝置的開機時間(不包括息屏睡眠時間),這個時間和Handler的訊息可以延遲觸發有關。將在後面詳細介紹。

Handler為什麼需要一個Looper,為什麼它不能為空?

因為MessageQueue是通過Looper獲取到的。
而Message需要通過MessageQueue來等待執行。
在建立Handler時如果檢測到Looper為空,將會丟擲NullPointerException錯誤

如果不是通過建構函式傳入的Looper,比如Handler#Callback,構造方法會通過Looper.myLooper()獲取到當前執行緒的Looper。

Looper.myLooper()可以獲取到當前執行緒的Looper是因為ThreadLocal的特性。

程式碼如下:

public Handler(@Nullable Callback callback, boolean async) {
    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;  
}

Handler為什麼可以做到執行緒間通訊?

Handler最長使用的大概是子執行緒通知UI執行緒更新UI吧。

我們通過post();sendMessage();等方法提交一個Message時,這個Message會被放入MessageQueue。在建立Handler時會得到一個Looper,Looper會迴圈從MessageQueue取出Message處理,而每個Looper屬於一個執行緒,如果該Looper是UI執行緒的,那Message就是在UI執行緒處理。

知其然亦應知其所以然。

我們從Handler#post()這個方法開始研究。
在上面已經講過,post()最終呼叫的是sendMessageAtTime(),這個方法首先是獲取了與該Handler繫結Looper的MessageQueue物件,然後通過一些引數設定,最後執行MessageQueue#enqueueMessage()方法。

MessageQueue#enqueueMessage()

看方法名就知道這個方法主要工作是對Message排隊處理:

boolean enqueueMessage(Message msg, long when) {  
	//... 
    synchronized (this) {  
        if (msg.isInUse()) {    
            throw new IllegalStateException(msg + " This message is already in use.");  
        }  
		//...
        msg.markInUse();  
        msg.when = when;  
        Message p = mMessages;  
        boolean needWake;  
        if (p == null || when == 0 || when < p.when) {  
            msg.next = p;  
            mMessages = msg;  
            needWake = mBlocked;  
        } else {  
            Message prev;  
            for (;;) {  
                prev = p;  
                p = p.next;  
                if (p == null || when < p.when) {  
                    break;  
                }
                //...
            }  
            msg.next = p; // invariant: p == prev.next  
            prev.next = msg;  
        } 
      //...
    }  
    return true;  
}

我去掉了一些和排隊無關的程式碼,上面這段程式碼並不難,就是以連結串列的形式將Message進行排列。
首先判斷這個Message是否正在被使用。

Message什麼情況下是被使用狀態呢?

其實這個也和Message回收池有些關係。
我們new的物件和回收池中取出的Message預設狀態是0,當Message進入MessageQueue等待處理時就是被使用狀態。從MessageQueue取出,被處理完成回收的Message其狀態又會被重置為0 。

回到Message排隊問題,然後判斷三個條件:

  1. Message連結串列是否為空
  2. when == 0
    1. 這種情況只有呼叫Handler#sendMessageAtFrontOfQueue()才會出現
  3. when < p.when
    1. when表示訊息將在什麼時候執行,數位越小的排在前面

當其中一個條件滿足時,將當前Message插入連結串列頭部。

既然Message時怎麼放入MessageQueue這塊已經弄清楚了,那接著看一下Looper。看看是怎麼取出Messages,又是怎麼處理的

Looper

如果使用過Looper就應該知道,這是一個迴圈。
檢視這個類的註釋,可以看到它提供了一個簡單的用法。

Looper.prepare();
Looper.loop();

主要有兩個方法:

  1. 第一個在當前執行緒建立Looper物件並放入ThreadLocal中,
  2. 第二個迴圈MessageQueue,取出其中的Message在當前執行緒處理

Looper實現在這裡不進行深入,只講一下和MessageQueue有關的。
先看loop():

//Looper
public static void loop() {  
    final Looper me = myLooper();  
    //...
    for (;;) {  
        if (!loopOnce(me, ident, thresholdOverride)) {  
            return;  
        }  
    }  
}
private static boolean loopOnce(final Looper me,  
        final long ident, final int thresholdOverride) {  
    Message msg = me.mQueue.next(); // might block  
    if (msg == null) {  
	    // No message indicates that the message queue is quitting.  
	    return false;  
	}
    //。。。
    msg.target.dispatchMessage(msg);  
	//。。。
    msg.recycleUnchecked();  
    return true;
}

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

可以看到loop()裡面用for寫了一個死迴圈,它執行了loopOnce(),它是真正取出Message並執行的方法。
dispatchMessage()方法內,首先判斷callback是否為空,它是Message的Runable,就是我們使用post();postDelayed();提交到Runable。
然後判斷Handler#Callback,是否為空,這個是在Handler構造方法傳入的Callback,這裡也解釋了當我們實現了callback時可以跨執行緒通訊的原因。

next()返回null會結束for迴圈。我們Android主執行緒沒有訊息為什麼還可以繼續執行?

我們建立Looper物件都是通過其靜態方法來建立的,而Looper的構造方法有一個引數quitAllowed,這個引數為True時MessageQueue不會因為訊息為空而退出。

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

postDelayed()為什麼可以讓執行緒延遲執行?

讓我們回到上面Looper#loopOnce()這個方法,我在上面沒有介紹怎麼從MessageQueue取出Message就是留給這個問題的。
線上關鍵程式碼:

//MessageQueue
Message next() {
	for (;;) {
        synchronized (this) {
	        //獲取當前的開機時間
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null) {
	            //判斷當前開機時間是否小於msg的開機時間
	            //如果為false表示這條訊息應該被拿出去處理了
                if (now < msg.when) {
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    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;
                }
            }
            //。。。
        }
	}
}

這塊程式碼還是很多的,我去掉了和該問題無關的程式碼。
這裡先獲取了當前開機時間,然後和Message的when對比,如果當前開機時間比when大,表示這條訊息到了處理時間,直接return。
我們使用postDelayed(Runnable,0)時,執行到sendMessageDelayed()後會加上SystemClock.uptimeMillis()最後變成when值,也就是SystemClock.uptimeMillis()+0
因此MessageQueue取出訊息時,該Message會被立即執行,而延遲xxx時間是一樣的道理。

Message回收池是怎麼回事?

讓我們回到上面展示的Looper#loopOnce()這個方法,可以看到當訊息被取出來處理後,呼叫了msg.recycleUnchecked();回收當前Message:

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

這個方法會重置Message引數,然後判斷當前回收池有沒有達到上限,上限是50個,沒有達到會把這個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.obtain()會檢查回收池,如果回收池不為空,從連結串列頭部取出一個物件並返回。
在Google官方檔案也能看到,Google建議我們通過Message.obtain()獲取一個新的Message物件,而不是直接new。

總結

handler的這個機制是由幾個類一起共同作業共同實現的。它們分別是:

  1. Handler
    • Handler:負責協調各個類的工作,以達到這個機制的功能。
  2. Looper
    • Looper:一個Looper物件屬於一個執行緒,由它來管理此執行緒裡的MessageQueue(訊息佇列)
  3. MessageQueue
    • MessageQueue:訊息佇列,負責管理Message,以及延遲Message處理
  4. Message
    • Message:用於存放需要傳送的資料,將封包裝為訊息物件。管理回收池等。

擴充套件知識

  1. MessageQueue