【Android自助餐】Handler訊息機制完全解析(二)MessageQueue的佇列管理

-_-void發表於2016-07-13

Android自助餐Handler訊息機制完全解析(二)MessageQueue的佇列管理

關於這個佇列先說明一點,該佇列的實現既非Collection的子類,亦非Map的子類,而是Message本身。因為Message本身就是連結串列節點(見Message中obtain()與recycle()的來龍去脈)。
佇列中的Message mMessages;成員即為佇列,同時該欄位直接指向佇列中下一個需要處理的訊息。

新增到訊息佇列enqueueMessage()

要將message新增到佇列除了提供message之外,還需提供訊息觸發時間when
如果當前佇列為空則直接mMessage=message即可。否則就需要逐個對比佇列中每個message的when和新訊息的when來確定新訊息在佇列中的位置。
先給出核心原始碼(有刪減)

Message p = mMessages;
if (p == null || when == 0 || when < p.when) {
    msg.next = p;
    mMessages = msg;
} else {
    Message prev;
    for (;;) {
        prev = p;
        p = p.next;
        if (p == null || when < p.when) {
            break;
        }
    }
    msg.next = p;
    prev.next = msg;
}
if (needWake) {
    nativeWake(mPtr);
}

先看下新訊息需要放到隊頭的情況:p == null || when == 0 || when < p.when。即佇列為空,或者新訊息需要立即處理,或者新訊息處理的事件比隊頭訊息更早被處理。這時只要讓新訊息的next指向當前隊頭,讓mMessages指向新訊息即可完成插入操作。
除了上述三種情況就需要遍歷佇列來確定新訊息位置了,下面結合示意圖來說明。
假設當前訊息佇列如下
初始狀態
開始遍歷:p向隊尾移,引入prev指向p上一個元素
prev
假設此時p所指訊息的when比新訊息晚,則新訊息位置在prev與p中間
插入
最後便是呼叫native方法來喚醒(Linux的epoll,有興趣的自行百度)。

從佇列取出訊息next()

這部分內容有點高能,請根據個人BPU(BrainProcessUnit)酌情理解。
首先這個方法需要返回Message,那麼我們現在來看看哪裡有return。(共三段,我們最後看第二段。)

第一段

final long ptr = mPtr;
if (ptr == 0) {
    return null;
}

如果mPtr為0則返回null。那麼mPtr是什麼?值為0又意味著什麼?在MessageQueue構造方法中呼叫了native方法並返回了mPtrmPtr = nativeInit();;在dispose()方法中將其值置0mPtr = 0;並且呼叫了nativeDestroy()。而dispose()方法又在finalize()中被呼叫。另外每次mPtr的使用都呼叫了native的方法,其本身又是long型別,因此推斷它對應的是C/C++的指標。因此可以確定,mPtr為一個記憶體地址,當其為0說明訊息佇列被釋放了。這樣就很容易理解為什麼mPtr==0的時候返回null了。

第三段

你沒有看錯,第二段在後面

if (mQuitting) {
    dispose();
    return null;
}

這裡的意思也很明顯,當這個訊息佇列退出的時候,返回空。而且在返回前呼叫了dispose()方法,顯然這意味著該訊息佇列將被釋放。

第二段

這部分涉及到的程式碼基本上就是這個next()方法本身了,但可以肯定的是這裡的返回語句是return msg;。同時從enqueueMessage()方法可以看出來,在這個佇列中取到的message物件不可能為空,因此這裡的返回絕對不為空。
如此一來就可以得出一個結論:如果next()方法為空說明這個訊息佇列正在退出或將被釋放回收。
繼續來看這個next(),這個程式碼有點長,所以先做個減法。
第一個要減的就是pendingIdleHandlerCount,這個區域性變數初始為-1,後面被賦值mIdleHandlers.size();。這裡的mIdleHandlers初始為new ArrayList<IdleHandler>(),在addIdleHander()方法中增加元素,在removeIdleHander()方法中移除元素。而我們所用的Handeler並未實現IdleHandler介面,因此在next()方法中pendingIdleHandlerCount的值要麼為0,要麼為-1,因此可以看出與該變數相關的部分程式碼執行情況是確定的,好的,把不影響迴圈控制的程式碼減掉。
第二個要減的是Binder.flushPendingCommands()這個程式碼看原始碼說明:

Flush any Binder commands pending in the current thread to the kernel driver. This can be useful to call before performing an operation that may block for a long time, to ensure that any pending object references have been released in order to prevent the process from holding on to objects longer than it needs to.

這段話啥意不懂也沒關係,這裡只需要知道:Binder.flushPendingCommands()方法被呼叫說明後面的程式碼可能會引起執行緒阻塞。然後把這段減掉。
第三個要減的是一個log語句if (DEBUG) Log.v(TAG, "Returning message: " + msg);
第四個要減的是上面提到的“第一段”返回null的語句,但是“第三段”得留著。
最後再把註釋幹掉給上程式碼:

Message next() {
    int nextPollTimeoutMillis = 0;
    for (;;) {
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    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 {
                nextPollTimeoutMillis = -1;
            }
            if (mQuitting) {
                dispose();
                return null;
            }
            if (pendingIdleHandlerCount <= 0) {//上面分析過該變數要麼為0要麼為-1
                mBlocked = true;
                continue;
            }
        }
        nextPollTimeoutMillis = 0;
    }
}

雖然還是很長,但也不能再減了。大致思路如下:先獲取第一個同步的message。如果它的when不晚與當前時間,就返回這個message;否則計算當前時間到它的when還有多久並儲存到nextPollTimeMills中,然後呼叫nativePollOnce()來延時喚醒(Linux的epoll,有興趣的自行百度),喚醒之後再照上面那樣取message,如此迴圈。程式碼中對連結串列的指標操作佔了一定篇幅,其他的邏輯很清楚,就不一句句分析了。

從佇列移除訊息removeMessages()

該方法有2個過載,除此之外還有removeCallbacksAndMessages()等方法也可以移除訊息。但程式碼段都基本一樣,這裡以void removeMessages(Handler h, int what, Object object){}方法為例。
該方法完整原始碼如下

void removeMessages(Handler h, int what, Object object) {
    if (h == null) {
        return;
    }

    synchronized (this) {
        Message p = mMessages;

        // Remove all messages at front.
        while (p != null && p.target == h && p.what == what
               && (object == null || p.obj == object)) {
            Message n = p.next;
            mMessages = n;
            p.recycleUnchecked();
            p = n;
        }

        // Remove all messages after front.
        while (p != null) {
            Message n = p.next;
            if (n != null) {
                if (n.target == h && n.what == what
                    && (object == null || n.obj == object)) {
                    Message nn = n.next;
                    n.recycleUnchecked();
                    p.next = nn;
                    continue;
                }
            }
            p = n;
        }
    }
}

最開始判斷handler是否為空不必多說,然後便是同步程式碼段,只裡面有兩個while迴圈。為什麼有兩個呢?學過資料結構連結串列的都知道,連結串列分兩種:帶頭結點和不帶頭結點。而這兩種連結串列的遍歷方式有所不同:不帶頭結點的連結串列中,第一個元素需要單獨處理,然後才能將後續部分當做帶頭結點的連結串列來使用while迴圈遍歷。可以看出MessageQueue是不帶頭結點的連結串列,而且遍歷過程中有需要刪除節點,因此要特殊處理的不只是第一個元素,而是第一組符合刪除條件的元素。有點暈了是吧,不要緊,我們開始鬥圖。

第一個while

假設需要遍歷的訊息佇列如圖所示。
初始狀態
為了讓第一個while可以執行,我們假設前3個元素符合移除條件,即前三個Message的targewhatobj分別與指定的handlerwhatobject相同。首先第一個元素滿足條件進行如下操作:
執行n=p.next;
n=p.next
後移mMessage;
後移mMessage
回收p指向的元素,即第一個元素。
回收p指向的元素
讓p指向新的隊頭。
讓p指向新的隊頭
此時又與初始佇列狀態一樣了。先前我們假設隊頭有三個元素符合移除條件,因此再迴圈執行上面4圖2邊後又得到初始狀態的佇列,此時隊頭元素不滿足移除條件因此while終止,同時新的佇列變成了“帶頭結點的連結串列”,因此mMessage指向的元素永遠不用被判斷是否滿足移除條件。

第二個while

此時訊息佇列狀態如下:
初始狀態
執行n=p.next;
n=p.next
假設n指向的元素不滿足移除條件,則只需要將p和n後移,如此也說明,p指向的元素總是已經被判斷過不滿足移除條件的。這部分邏輯很簡單到給圖就是看不起讀者的智商,現在我們假設n指向的元素滿足移除條件,即當前佇列如下:
滿足條件
執行nn=n.next;
nn=n.next
回收n指向的元素
回收n指向的元素
執行p.next=nn;
p.next=nn
這時p之後的佇列又是一個帶頭結點的連結串列。可以繼續while了。

相關文章