Android Handler之同步屏障機制(sync barrier)

Handler Message種類

Handler的Messgae種類分爲三種:

  • 普通消息
  • 異步消息
  • 屏障消息
    其中普通消息又稱爲同步消息,我們平時發的消息基本都是同步消息,在這裏不做討論。

異步消息

通常我們使用Handler想消息隊列中添加的Message都是同步的,如果我們想要添加一個異步的Message,有以下兩種方式:

  • 1、Handler的構造方法有個async參數,默認的構造方法此參數是false,只要我們在構造handler對象的時候,把該參數設置爲true就可以了。
 public Handler(Callback callback, boolean async) {
       ......省略代碼
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

可以看到,async設置爲true後,對全局的mAsynchronous設置爲true。然後在enqueueMessage()方法裏,調用msg.setAsynchronous(true),將message設置爲異步的。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
  • 2、在創建Message對象時,直接調用Message的setAsynchronous()方法。
    在一般情況下,異步消息和同步消息沒有什麼區別,但是一旦開啓了同步屏障以後就有區別了。

同步屏障

一般來說,MessageQueue裏面的所有Message是按照時間從前往後有序排列的。

同步屏障消息就是在消息隊列中插入一個屏障,在屏障之後的所有普通消息都會被擋着,不能被處理。不過異步消息卻例外,屏障不會擋住異步消息,因此可以認爲,屏障消息就是爲了確保異步消息的優先級,設置了屏障後,只能處理其後的異步消息,同步消息會被擋住,除非撤銷屏障。

同步屏障是通過MessageQueue的postSyncBarrier方法開啓的。

private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
            // 1
        final int token = mNextBarrierToken++;
        // 2
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;
                // 3
                // 指向前一個Message
        Message prev = null;
        // 消息隊列中的第一個Message賦值給p
        Message p = mMessages;
        if (when != 0) {
        // 4 通過p的時間和屏障的時間,確定屏障消息插入的位置
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        // 5 說明屏障消息不是插入消息隊列的頭部
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
        // 6 屏障消息在消息隊列的頭部
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}
  • 第一步,獲取屏障的的唯一標示,標示從0開始,自加1。
  • 第二步,從Message消息對象池中獲取一個msg,設置msg爲正在使用狀態,並且重置msg的when和arg1,arg1的值設置爲token值。但是這裏並沒有給tareget賦值。所以msag的target是否爲空是判斷這個msg是否是屏障消息的標誌。
  • 第三步,創建變量pre和p,爲下一步做準備。其中p被賦值爲mMessages,mMessages指向消息隊列中的第一個元素,所以此時p指向消息隊列中的第一個元素。
  • 第四步,通過對隊列中的第一個Message的when和屏障的when進行比較,決定屏障消息在整個消息隊列中的位置,因爲消息隊列中的消息都是按時間排序的。
  • 第五步,prev != null,代表不是消息的頭部,把msg插入到消息隊列中。
  • 第六步,prev == null,代表是消息隊列的頭部,把msg插入消息的頭部。

我們通常通過Handler發送消息handler.sendMessage(),最終都會調用Handler.java中的enqueueMessage()方法。

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

可以看到,enqueueMessage()方法裏爲msg設置了target字段。
而上面的postSyncBarrier(),也是從Message消息對象池中獲取一個msg,插入到消息隊列中,唯一的不同是沒有設置target字段。所以從代碼層面上講,屏障消息就是一個target爲空的Message。

屏障消息的工作原理

通過postSyncBarrier方法屏障就被插入到消息隊列中了,那麼屏障是如何擋住普通消息只允許異步消息通過的呢?

我們知道Handler的消息處理是在Looper.loop()從消息隊列中獲取消息,並交給Handler處理的,其中是通過MessageQueue是通過next方法來獲取消息的。查看一下next()的源碼,

Message next() {
   // .....省略代碼
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    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
            Message prevMsg = null;
            // 初始時指向第一個message
            Message msg = mMessages;
            // 1 msg.target == null說明遇到消息屏障
            if (msg != null && msg.target == null) {
                    // 能進入這個if,說明此時的msg是屏障消息
                    // 循環遍歷,退出循環的條件是,message到末尾了,或者
                    // msg是異步消息
                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 {
          // Got a message.
          mBlocked = false;
          if (prevMsg != null) {
          // 將msg從消息鏈表中移除
                 prevMsg.next = msg.next;
              } else {
                mMessages = msg.next;
              }
            msg.next = null;
           if (DEBUG) Log.v(TAG, "Returning message: " + msg);
            msg.markInUse();
            // 返回msg
            return msg;
         }

從上面的代碼可以看出,msg.target == null時說明此時的msg是屏障消息,此時會進入到循環,遍歷移動msg的位置,知道移動到的msg是異步message則退出循環,也就是說,循環的代碼會過濾掉所有的同步消息,直到取出異步消息爲止。

當設置了同步屏障之後,next函數將會忽略所有的同步消息,返回異步消息。換句話說就是,設置了同步屏障之後,Handler只會處理異步消息。再換句話說,同步屏障爲Handler消息機制增加了一種簡單的優先級機制,異步消息的優先級要高於同步消息。

移除同步屏障

同步屏障的移除是在MessageQueue.java的removeSyncBarrier()方法。

public void removeSyncBarrier(int token) {
    // Remove a sync barrier token from the queue.
    // If the queue is no longer stalled by a barrier then wake it.
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        // 循環遍歷,直到遇到屏障消息時推退出循環
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }
        final boolean needWake;
        if (prev != null) {
        // 刪除屏障消息p
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        p.recycleUnchecked();

        // If the loop is quitting then it is already awake.
        // We can assume mPtr != 0 when mQuitting is false.
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

刪除屏障消息的方法很簡單,就是不斷遍歷消息隊列,知道找到屏障消息,退出循環的條件有兩個,一是p.target == null,說明是屏障消息,二是p.arg1 == token,也說明p是屏障消息,因爲在屏障消息入隊的時候,設置過 msg.arg1 = token。找到屏障消息後,把它從消息隊列中刪除並回收。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章