Handler詳解3-MessageQueue和異步消息


MessageQueue

  MessageQueue(消息隊列)是Message(消息)的管理者,它負責保存消息的集合,執行消息入隊、出隊等操作,同時提供SyncBarrier(同步障礙器)與IdleHandler(閑時任務)機制。SyncBarrier機制允許我們暫停部分Message的出隊,而IdleHandler機制允許我們在沒有消息需要出隊處理時執行一些簡單的任務。

1.MessageQueue的創建

消息隊列只含一個構造方法,其代碼如下:

MessageQueue(boolean quitAllowed) {
      mQuitAllowed = quitAllowed;
      mPtr = nativeInit();
}

 

  在Java中如果定義類的方法時不設置方法的訪問權限,那么方法默認的訪問權限不是public,不是private,也不是protected。方法默認的訪問權限允許同一個包下的類訪問該方法,但不允許子類繼承與訪問。翻看調用記錄可發現,消息隊列所在的Android.os包下只有Looper類調用過消息隊列的構造方法,而消息隊列中並沒有諸如緩存池之類的結構可以獲取已經構造好的消息隊列。我們可以斷定消息隊列是依附在Looper上的消息對象集合,想要獲得消息隊列必須通過Looper(Looper.myQueue())。 
  quitAllowed指定消息隊列是否允許退出,其值由Looper類指定,在Looper關聯的線程是主線程時才為false

 

2.同步消息與異步消息

  同異步消息的區別有兩點。第一點,同步消息自始至終都會按照順序執行(when相同的消息,哪個先入隊就先執行哪個),異步消息的執行順序則完全不確定。第二點,同步消息會被同步障礙器攔截而異步消息不會受到影響。 
  第一點出自官方文檔(Message.isAsynchronous()注釋),暫未找到可以佐證的代碼塊。第二點,我們可以從SyncBarrier的工作原理中得到佐證。 
  通常我們會把中斷消息、事件消息等較為重要的Message設置為異步消息,以保證系統能夠盡早處理這些消息

 

3.SyncBarrier(同步障礙器)

先上結論:SyncBarrier是特殊的消息對象,其特征是target字段為null且arg1字段保存token,其作用是阻礙消息隊列使其在處理普通消息時直接跳過位於SyncBarrier后的所有 同步 消息。 
我們先來看看SyncBarrier是怎么定義的:

 /**
     * 將同步障礙器加入消息隊列。如果此時消息隊列處於阻塞狀態也不需要喚醒,因為障礙器本身的目的就是
     * 阻礙消息隊列的循環處理。(可以假設一下為什么阻塞,各種阻塞場景下需不需要喚醒)
     * @param when 同步障礙器從何時起效(這個時間是自系統啟動開始算起,到指定時間的不包含深度睡
     *             眠的毫秒數)。
     * @return  新增的同步障礙器token,用於{@link #removeSyncBarrier(int) }移除障礙器時使用
     * */
    int postSyncBarrier(long when) {
        synchronized (this) {
            final int token = mNextBarrierToken++;
            //從消息池取出一個消息,並將其設置為同步障礙器(target為null,且arg1保存token的消息)
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            //找到msg在消息隊列中的位置(消息隊列按照when從小到大排列),並把msg插入其中
            …………(省略)
            return token;
        }
    }

 

 

  看到這里想必大家有一個疑問:難道普通消息的terget不可能是null嗎?我們目光移到MessageQueue.enqueueMessage(Message,long)方法上,這個方法是唯一能夠從隊列外部將一個普通消息加入隊列的方法,方法體中的第一行便有這樣一段代碼:

if (msg.target == null) {
      throw new IllegalArgumentException("Message must have a target.");
}

 

  如果准備加入隊列的Message.target為null,加入操作會拋出異常。因此我們可以斷定,消息隊列中target為null的消息一定是SyncBarrier。

 

 

繼續搜索:

 /**
     *得到下一個等待處理的消息。如果當前消息隊列為空或者下一個消息延時時間未到則阻塞線程。
     *
     * @return  <em>null</em> 消息隊列已經退出或者被廢棄
     */
Message next() {
   …………(略)
   for (;;) {
      …………(略)
      synchronized (this) {
      // Try to retrieve the next message.  Return if found.
      //now等於自系統啟動以來到此時此刻,非深度睡眠的時間
      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());
      }
      …………(略)
  }//for(;;)結束
}

 

 

  如果隊首是SyncBarrier,next()在尋找下一個待處理的普通消息時遇到同步消息會直接跳過,遇到異步消息才會繼續執行。這段代碼證實了我們之前的結論。繼續搜索"target == null",除了入隊拋異常時又出現了一次外再也沒出現過了。

 

異步消息和SyncBarrier

為了讓View能夠有快速的布局和繪制,Android中定義了一個SyncBarrier的概念,當View在繪制和布局時會向MessageQueue中添加了Barrier(監控器),這樣后續的消息隊列中的同步的消息將不會被執行,以免會影響到UI繪制,但是只有異步消息才能被執行。

    所謂的異步消息也只是體現在這,添加了Barrier后,消息還可以繼續被執行,不會被推遲運行。

如何使用異步消息?

異步消息是不能使用的,因為相關設置都是hide。

handler的相關構造函數,hide

Mesasge.setAsynchronous(boolean),hide

 

假設我們可以使用,則 在創建Handler時,可以在構造方法上傳遞參數true,如下:

    public Handler(boolean async) {
        this(null, async);
}

 

 

在Handler上設置異步的話,那么使用此Handler發送的消息都將被設置Mesasge#setAsynchronous(true)

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

 

 

當ViewRootImpl開始measure和layout ViewTree時,會向主線程的Handler添加Barrier

    void scheduleTraversals() {  
        if (!mTraversalScheduled) {  
            mTraversalScheduled = true;  
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();  
            mChoreographer.postCallback(  
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);  
            scheduleConsumeBatchedInput();  
        }  
    }  

    void unscheduleTraversals() {  
    if (mTraversalScheduled) {  
        mTraversalScheduled = false;  
        mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);  
        mChoreographer.removeCallbacks(  
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);  
} 

 

 

scheduleTraversals()方法都會在requestLayout時會被調用,所有導致布局變化的都會觸發。例如:ViewGroup#addView,removeView或者View 從VISIBLE-->GONE,或者GONE-->VISIBLE等。

 

4.next()

  next()是消息隊列中最為核心的方法,Looper從消息隊列中取消息就是通過next()實現的。next()保證每次調用都能讓Looper得到一個消息,除非消息隊列正在退出或者已經廢棄(此時返回null)。也就是說如果暫時取不出消息,next()並不會返回!此時為了節省資源,next()會根據消息隊列的情況設定阻塞時長然后再阻塞線程。 
 消息隊列(未退出、未被廢棄)在以下四種情況下,next()會選擇阻塞線程: 
  
1
)隊列中沒有任何消息
– 永久阻塞:這個時候不能返回null,因為next()的目的是取出一個消息,隊列中現在沒有消息並不代表一段時間后也沒有消息。消息隊列還在可用中,隨時都有可能有Handler發布新的消息給它。那么問題來了,為了節省資源准備阻塞線程但是多少時間后喚醒它呢?臆斷一個時長並不是很好的解決方案。我們知道消息隊列是用來管理消息的,既然確定不了阻塞時長那么不如先永久阻塞,等新消息入隊后主動喚醒線程。 
  2)隊首的消息執行時間未到 – 定時喚醒:每個消息的when字段都給出了希望系統處理該消息的時刻。如果在next()方法取消息時,發現消息隊列的隊首消息處理時間未到,next()同樣需要阻塞。因為消息隊列是按照消息的when字段從小到大排列的,如果隊首消息的處理時間都沒到那么整個隊列中都沒有能夠立即取出的消息。這個時候因為知道下一次處理的具體時間,結合當前時間就可以確定阻塞時長。 
  3)隊首消息是同步障礙器(SyncBarrier),並且隊列中不含有異步消息 – 永久阻塞:因為對消息隊列施加了同步障礙,所有晚於隊首同步障礙器處理時間的同步消息都變得不可用,next()在選取返回消息時會完全忽略這些消息。這和第一種情況相似,所以采取的阻塞方案也是永久阻塞。 
  4)隊首消息是同步障礙器(SyncBarrier),隊列中含有異步消息但執行時間未到 – 定時喚醒:因為對消息隊列施加了同步障礙,所有晚於隊首同步障礙器處理時間的同步消息都變得不可用,next()在選取返回消息時會完全忽略這些消息。也就是說對於next(),它只會考慮隊列中的異步消息。這和第二種情況相似,所以采取的阻塞方案是設置阻塞時長再阻塞。

 

在這個基礎上,我們再以驗證的方式來看next()方法的源代碼:

 

5.IdleHandler(閑時任務)

  IdleHandler允許我們在消息隊列空閑時執行一些不耗時的簡單任務。

我們來看看消息隊列是怎么使用IdleHandler的,首先目光轉到成員變量上,我們可以發現有這樣兩個成員:

  mIdleHandlers可以理解成是一個IdleHandler的總列表,每次next()將要執行IdleHandler時都會從這個總列表中取出所有的IdleHandler。mPendingIdleHandlers指定哪些IdleHandler需要在本次執行中完成,每次next()將要執行IdleHandler時都會從mIdleHandlers拷貝IdleHandler的總列表到mPendingIdleHandlers中。

 

下面這一段代碼是MessageQueue.next()中用於處理IdleHandler的代碼段:

  執行IdleHandler其實就是調用其queueIdle()方法,queueIdle()如果返回false,next()方法會將該IdleHandler從mIdleHandlers中刪除。這樣的話,下一次next()方法再執行IdleHandler時就不會再重復執行它了。 
  需要特別提醒的是,雖然next()方法是一個無限for循環,但是每次調用next()都只會執行一次mIdleHandlers中的閑時任務。因為在上面的代碼段之前有這樣一段:

 

 

6.主動喚醒

  針對上一節提到的四種阻塞情況,我們來分析一下對應情況下什么時候才需要主動喚醒: 
  1)隊列中沒有任何消息 – 永久阻塞:新消息入隊后便主動喚醒線程,無論新消息是同步消息、異步消息。 
  2)隊首的消息執行時間未到 – 定時喚醒:如果在阻塞時長未耗盡時,就新加入早於隊首消息處理時間的消息,需要主動喚醒線程。 
  3)隊首消息是同步障礙器(SyncBarrier),並且隊列中不含有異步消息 – 永久阻塞:如果新加入的消息仍然是晚於隊首同步障礙器處理時間,那么這次新消息的發布在next()層面上是毫無意義的,我們也不需要喚醒線程。只有在新加入早於隊首同步障礙器處理時間的同步消息時,或者,新加入異步消息時(不論處理時間),才會主動喚醒被next()阻塞的線程。 
  4)隊首消息是同步障礙器(SyncBarrier),隊列中含有異步消息但執行時間未到 – 定時喚醒:因為隊首同步障礙器的緣故,無論新加入什么同步消息都不會主動喚醒線程。即使加入的是異步消息也需要其處理時間早於設定好喚醒時執行的異步消息,才會主動喚醒。

 

  上面的討論中,我們只考慮了新增普通消息時,是否需要主動喚醒阻塞中的線程。現在我們來考慮一下,移除普通消息時是否應該喚醒。第一種情況沒有消息跳過。第二種情況和第四種情況下,假設我們移除設定好下次被動喚醒時執行的消息,線程被喚醒后就會因為沒有需要處理的消息而再次進入阻塞,並不會錯過消息所以不需要主動喚醒。第三種情況下移除設定好下次被動喚醒時執行的消息,線程雖然會再次進入阻塞但並不會錯過消息,也不需要主動喚醒。所以,移除普通消息在任何情況下都不需要主動喚醒線程。 
  如果是新增和移除同步障礙器呢?無論哪種情況,新增的同步障礙器都會在被動喚醒時發揮同步障礙的作用,不會因為沒有主動喚醒而多處理不該處理的消息,所以新增同步障礙器之后不需要主動喚醒線程。針對第三種和第四種情況,移除隊首障礙器能夠使本不可取出的同步消息變得可用,需要主動喚醒線程重新判斷是否能夠取出消息或者是否需要縮短阻塞時長。除非新的隊首消息還是同步障礙器才不需要喚醒! 
  至於加長阻塞時長使線程不會被無謂地被動喚醒(因為移除消息或者障礙器),這個設定至少在API22之前還沒寫入到代碼中。

 

就着上面給出的結論,我們來看看Android是怎么實現這些設定的。首先目光回到MessagqeQueue.enqueueMessage(),來看看新增普通消息時是怎么判斷是否需要主動喚醒的:

  在這個方法中,出現了一種我們前文都沒提到的一種主動喚醒情況 —— when==0(立即執行),實際上這也是毫無疑問需要主動喚醒的一種情況。對於第一種情況,新加入消息肯定需要主動喚醒;對於第二種,不主動喚醒會錯過;對於第三、第四種情況,隊首的同步障礙器不能影響早於它執行的消息,所以新加入when為0的消息無論如何都能夠執行,如果不主動喚醒也會錯過!所以,無論什么情況,只要新入隊的消息when字段為0,都要主動喚醒線程! 
  移除消息的MessageQueue.removeMessages()系列和MessageQueue.removeCallbacksAndMessages()方法,雖然可能導致線程下次被動喚醒時沒有消息執行,但是都不會錯過消息所以不需要主動喚醒。 
  接下來目光轉到同步障礙器上,MessageQueue.removeSyncBarrier()代碼:

  "needWake = mMessages == null || mMessages.target != null",這個語句含有一種有趣的情況。當消息隊列中不含普通消息只含一個同步障礙器時,移除這個障礙器后整個消息隊列都空了。按理說,移除之前next()線程已經處於無限阻塞中,移除后再喚醒結果還是無線阻塞。從消息處理上來講,這是一個可以輕松避免且毫無意義的喚醒。從空閑任務的管理上來講,next()方法在阻塞線程之前都會執行空閑任務然后再迭代一次判斷是否阻塞,阻塞后再喚醒也不可能在本次next()中再執行一次空閑任務,依然是一個可以輕松避免且毫無意義的喚醒。 
  另外一點是,這段代碼並沒有融合mBlocked(記錄當前線程是否阻塞)變量的值。可能出現線程未阻塞時主動喚醒線程的無謂舉動。 
  這兩個問題也許在看到本地方法nativeWake()的定義之后才能解開,暫時留下疑問。

 

7.消息隊列的退出與廢棄

  當Looper對象退出循環處理時,會調用MessageQueue的同包成員方法quit(safe)通知消息隊列開始退出操作。如果boolean型的參數safe是true,消息隊列會清除when晚於當前時間的所有同步/異步消息與同步障礙器,留下本應處理完的消息繼續處理;如果safe是false,則完全不顧慮,清除消息隊列中的所有消息。 
  在next()方法執行過程中,如果處理完隊列中全部消息后發現該消息隊列的quit()方法被調用過,則直接調用dispose()廢棄消息隊列並返回null給Looper。當GC回收消息隊列之前,會調用消息隊列重載的finalize()方法,在這個方法中同樣能夠執行廢棄消息隊列的操作(如果還未廢棄)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM