Android Handler機制之Message及Message回收機制

AndyJennifer發表於2018-09-22

小松鼠.jpg

該文章屬於《Android Handler機制之》系列文章,如果想了解更多,請點選 《Android Handler機制之總目錄》

前言

在前面的文章中我們講解了Handler、Looper、MessageQueue的具體關係,瞭解了具體的訊息迴圈的流程。下面將一起來探討最為整個訊息迴圈的訊息載體Message。

Message中可以攜帶的資訊

Message中可以攜帶的資料比較豐富,下面對一些常用的資料進行了分析。

/**
 * 使用者定義的訊息程式碼,以便當接受到訊息是關於什麼的。其中每個Hanler都有自己的命名控制元件,不用擔心會衝突
 */	
 public int what;
/**
 * 如果你只想存很少的整形資料,那麼可以考慮使用arg1與arg2,
 * 如果需要傳輸很多資料可以使用Message中的setData(Bundle bundle)
 */
 public int arg1;
/**
 * 如果你只想存很少的整形資料,那麼可以考慮使用arg1與arg2,
 * 如果需要傳輸很多資料可以使用Message中的setData(Bundle bundle)
 */
 public int arg2;
/**
 * 傳送給接受方的任意物件,在使用跨程式的時候要注意obj不能為null
 */
 public Object obj;
/**
 * 在使用跨程式通訊Messenger時,可以確定需要誰來接收
 */
 public Messenger replyTo;
/**
 * 在使用跨程式通訊Messenger時,可以確定需要發訊息的uid
 */
 public int sendingUid = -1;
/**
 * 如果資料比較多,可以直接使用Bundle進行資料的傳遞
 */
 Bundle data;
複製程式碼

其中關於what的值為什麼不會衝突的原因是,之前我們講過的handler是與執行緒進行繫結的。也就是說不同訊息迴圈訊息的傳送,處理的執行緒是不一樣的。當然是不會衝突的。對於Messenger,因為涉及到Binder機制,這裡就不過多的描述了,有興趣的小夥伴可以自行查詢相關資料學習。

建立訊息的方式

官方建議使用Message.obtain()系列方法來獲取Message例項,因為其Message例項是直接從Handler的訊息池中獲取的,可以迴圈利用,不必另外開闢記憶體空間,效率比直接使用new Message()建立例項要高。其中具體建立訊息的方式,我已經為大家分好類了。具體分類如下:

//無引數
public static Message obtain() {...}
//帶Messag引數
public static Message obtain(Message orig) {}
//帶Handler引數
public static Message obtain(Handler h) {}
public static Message obtain(Handler h, Runnable callback){}
public static Message obtain(Handler h, int what){}
public static Message obtain(Handler h, int what, Object obj){}
public static Message obtain(Handler h, int what, int arg1, int arg2){}
public static Message obtain(Handler h, int what,int arg1, int arg2, Object obj) {}
複製程式碼

其中在Message的obtain帶引數的方法中,內部都會呼叫無參的obtain()方法來獲取訊息後。然後並根據其傳入的引數,對Message進行賦值。(關於具體的obtain方法會在下方訊息池實現原理中具體描述)

訊息池實現原理

既然官方建議使用訊息池來獲取訊息,那麼在瞭解其內部機制之前,我們來看看Message中的訊息池的設計。具體程式碼如下:

private static final Object sPoolSync = new Object();//控制獲取從訊息池中獲取訊息。保證執行緒安全
private static Message sPool;//訊息池
private static int sPoolSize = 0;//訊息池中回收的訊息數量
private static final int MAX_POOL_SIZE = 50;//訊息池最大容量
複製程式碼

從Message的訊息池設計,我們大概能看出以下幾點:

  1. 該訊息池在同一個訊息迴圈中是共享的(sPool宣告為static),
  2. 訊息池中的最大容量為50,
  3. 從訊息池獲取訊息是執行緒安全的。

從訊息池中獲取訊息

在上文中,我們已經知道了在使用訊息池獲得訊息時,都會呼叫無參的obtain()方法。具體程式碼如下:

 public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; //重新標識當前Message沒有使用過
                sPoolSize--;
                return m;
            }
        }
        return new Message();//如果為空直接返回
    }
複製程式碼

從上述程式碼中,我們可以瞭解,也就是當前 訊息池不為空(sPool !=null)的情況下,那麼我們就可以從訊息池中獲取資料,相應的訊息池中的訊息數量會減少。訊息池的內部實現是以連結串列的形式,其中spol指標指向當前連結串列的頭結點,從訊息池中獲取訊息是以移除連結串列中sPool所指向的節點的形式,具體原理如下圖所示:

獲取訊息.png

回收訊息到訊息池

在Meaage的訊息回收中,訊息的實際回收方法是recycleUnchecked()方法,具體如下圖所示:

   void recycleUnchecked() {
	    //用於表示當前Message訊息已經被使用過了
        flags = FLAG_IN_USE;
        //情況之前Message的資料
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;
		//判斷當前訊息池中的數量是不是小於最大數量,其中 MAX_POOL_SIZE=50
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;//記錄當前訊息池中的數量
            }
        }
    }
複製程式碼

在recycleUnchecked()方法中,大致分為三步,第一步將該條回收的訊息狀態設定為正在使用,第二步將Message所有的儲存資訊都變為初始值,第三步,如果當前訊息池仍能夠儲存回收的訊息,那麼就將訊息儲存在訊息池中。其中將回收訊息加入訊息池中是使用連結串列的形式,具體回收訊息到訊息池如下圖所示:

加入訊息.png

Message 訊息回收時機

這裡為了方便大家梳理邏輯,我提前將幾種會呼叫訊息進行回收的情況都描述出來了,具體的情況如下所示:

當Handler指定刪除單條訊息,或所有訊息的時候

void removeMessages(Handler h, int what, Object object)
void removeMessages(Handler h, Runnable r, Object object)
void removeCallbacksAndMessages(Handler h, Object object)
複製程式碼

當使用Handler刪除某條訊息的時候,會分別呼叫MessageQueue的 removeMessages(Handler h, int what, Object object)與removeCallbacksAndMessages(Handler h, Object object) ,removeMessages(Handler h, Runnable r, Object object) 三個方法。這三個個方法邏輯比較類似。這裡直接選取removeCallbacksAndMessages()方法來進行講解。具體程式碼如下:

 void removeCallbacksAndMessages(Handler h, Object object) {
        if (h == null) {
            return;
        }

        synchronized (this) {
            Message p = mMessages;

            // 回收滿足條件的第一條訊息  第一步
            while (p != null && p.target == h
                    && (object == null || p.obj == object)) {
                 //下面操作會將滿足回收條件的訊息,從訊息佇列中移除
                Message n = p.next;
                mMessages = n;
                p.recycleUnchecked();
                p = n;
            }

            // 回收該條訊息後面的滿足條件的訊息 第二步
            while (p != null) {
                Message n = p.next;
                if (n != null) {
                    if (n.target == h && (object == null || n.obj == object)) {
	                    //下面操作會將滿足回收條件的訊息,從訊息佇列中移除
                        Message nn = n.next;
                        n.recycleUnchecked();
                        p.next = nn;
                        continue;
                    }
                }
                p = n;
            }
        }
    }
複製程式碼

在removeCallbacksAndMessages(Handler h, Object object)方法中,在該方法中分成了兩步,

  • 第一步:回收滿足條件的第一條訊息。同時將該訊息從訊息佇列中移除。並且將mMessages指向訊息佇列中的頭節點。 在第一步中,我們可以看出會迴圈遍歷訊息佇列中的訊息找到p.target == h&&((object == null || p.obj == object),然後進行回收,也就是說在第一步中,會移除對應的Handler。(在Handler機制中,多個handler對應同一個MessageQueue,對應同一個Looper,Handler與MessageQueue與Looper之間的關係是N:1:1)
  • 第二步:回收已經回收的第一條訊息之後所有滿足條件的訊息。同時將這些訊息從訊息佇列中移除。

思考:為什麼不直接走第二步回收訊息就行了。反正滿足條件的訊息都會移除,為毛要先移除第一條,在接著移除後面的訊息(這裡如果大家感到困惑,請仔細觀看第一步操作中的 其中一條語句mMessages = n;,之所以會走兩次迴圈,主要目的是讓mMessages指向訊息佇列中的頭節點)。

當Loooper取出訊息時

    public static void loop() {
		 //省略部分程式碼
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
			//省略部分程式碼
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
		    //省略部分程式碼
		    
		    //回收訊息
            msg.recycleUnchecked();
        }
    }
複製程式碼

我們都知道訊息的取出是通過Looper類中的loop方法。從程式碼中我們可以看出,當訊息取出並執行相應操作後。最後會將訊息回收。

當Looper取消迴圈訊息佇列的時候

public void quitSafely() { mQueue.quit(true);}
public void quit() { mQueue.quit(false); }
複製程式碼

當退出訊息佇列的時候,也就是呼叫Loooper的quitSafely()或quit()方法,從程式碼中我們可以看出,會呼叫其內部的MessageQueue的quit(boolean safe)方法。我們繼續跟蹤程式碼。

   void quit(boolean safe) {
        if (!mQuitAllowed) {//注意,主執行緒是不能退出訊息迴圈的
            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);
        }
    }
複製程式碼

在MessageQueue的quit(boolean safe)方法中,會將mQuitting (用於判斷當前訊息佇列是否已經退出)置為true,同時會根據當前是否安全退出的標誌 (safe)來走不同的邏輯,如果安全則走removeAllFutureMessagesLocked()方法,如果不是安全退出則走removeAllMessagesLocked()方法。下面分別對這兩個方法進行討論。

非安全退出
    private void removeAllMessagesLocked() {
        Message p = mMessages;
        while (p != null) {
            Message n = p.next;
            p.recycleUnchecked();
            p = n;
        }
        mMessages = null;
    }
複製程式碼

非安全退出其實很簡單,就是將所有訊息佇列中的訊息全部回收。具體示意圖如下所示:

回收全部訊息.png

安全退出
   private void removeAllFutureMessagesLocked() {
        final long now = SystemClock.uptimeMillis();
        Message p = mMessages;//當前佇列中的頭訊息
        if (p != null) {
            if (p.when > now) {//判斷時間,如果Message的取出時間比當前時間要大直接移除
                removeAllMessagesLocked();
            } else {
                Message n;
                for (;;) {//繼續判斷,取佇列中所有大於當前時間的訊息
                    n = p.next;
                    if (n == null) {
                        return;
                    }
                    if (n.when > now) {
                        break;
                    }
                    p = n;
                }
                p.next = null;
                do {//將所有所有大於當前時間的訊息的訊息回收
                    p = n;
                    n = p.next;
                    p.recycleUnchecked();
                } while (n != null);
            }
        }
    }
複製程式碼

觀察上訴程式碼,在該方法中,會判斷當前訊息佇列中的頭訊息的時間是否大於當前時間,如果大於當前時間就會removeAllMessagesLocked()方法(也就是回收全部訊息),反之,則回收部分訊息,同時沒有被回收的訊息任然可以被取出執行。具體示意圖如下所示:

回收部分訊息.png

當訊息佇列退出的,但是仍然傳送訊息過來的時候

在Looper呼叫quit()方法時,也就是Looper退出訊息迴圈的時候,我們已經知道了其內部會呼叫MessageQueue的quit(boolean safe)方法。當MessageQueue退出的時候,會將mQuitting置為true。那麼當對應的Handler傳送訊息時,我們都知道會呼叫MessageQueue的enqueueMessage(Message msg, long when)方法。那麼現在我們觀察下列程式碼:

boolean enqueueMessage(Message msg, long when) {
	   ...省略部分程式碼
        synchronized (this) {
          ...省略部分程式碼
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }
            ...省略部分程式碼
     }
複製程式碼

觀察該程式碼我們得知,當迴圈訊息退出的時候,如果這個時候Handler繼續傳送訊息來。會將該訊息回收。但是現在這裡有個問題。既然我們的訊息佇列已經結束迴圈了。那麼我們回收該訊息又有什麼用呢?我們又不能重新的開啟訊息迴圈。不知道Google這裡為什麼會這麼設計。

總結

  • 在使用Handler發訊息時,建議使用Message.obtin()方法,從訊息池中獲取訊息。
  • 在Message中訊息池是使用連結串列的形式來儲存訊息的。
  • 在Message中訊息池中最大允許儲存50條的訊息。
  • 在使用Handler移除某條訊息的時候,該訊息有可能會被訊息池回收。(會判斷訊息池是否仍然能儲存訊息)

相關文章