天天看點

Handler sync barrier(同步屏障)

Handler中的Message可以分為兩類:同步消息、異步消息。消息類型可以通過以下函數得知

//Message.java
public boolean isAsynchronous() {
    return (flags & FLAG_ASYNCHRONOUS) != 0;
}
           

一般情況下這兩種消息的處理方式沒什麼差別,隻有在設定了同步屏障時才會出現差異。

1 什麼是同步屏障

同步屏障可以通過MessageQueue.postSyncBarrier函數來設定

/**
     *
     * @hide
     */
    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    private int postSyncBarrier(long when) {
        synchronized (this) {
            final int token = mNextBarrierToken++;
            //1、屏障消息和普通消息的差別是屏障消息沒有tartget。
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            //2、根據時間順序将屏障插入到消息連結清單中适當的位置
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            //3、傳回一個序号,通過這個序号可以撤銷屏障
            return token;
        }
    }
           

postSyncBarrier方法就是用來插入一個屏障到消息隊列的,可以看到它很簡單,從這個方法我們可以知道如下:

  • 屏障消息和普通消息的差別在于屏障沒有tartget,普通消息有target是因為它需要将消息分發給對應的target,而屏障不需要被分發,它就是用來擋住普通消息來保證異步消息優先處理的。
  • 屏障和普通消息一樣可以根據時間來插入到消息隊列中的适當位置,并且隻會擋住它後面的同步消息的分發。
  • postSyncBarrier傳回一個int類型的數值,通過這個數值可以撤銷屏障。
  • postSyncBarrier方法是私有的,如果我們想調用它就得使用反射。
  • 插入普通消息會喚醒消息隊列,但是插入屏障不會。

可以看到,Message 對象初始化的時候沒有給 target 指派,是以, 

target == null

的 來源就找到了。上面消息的插入也做了相應的注釋。這樣,一條

target == null

 的消息就進入了消息隊列。

該函數僅僅是建立了一個Message對象并加入到了消息連結清單中。乍一看好像沒什麼特别的,但是這裡面有一個很大的不同點是該Message沒有target。

我們通常都是通過Handler發送消息的,Handler中發送消息的函數有post***、sendEmptyMessage***以及sendMessage***等函數,而這些函數最終都會調用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字段。

是以,從代碼層面上來講,同步屏障就是一個Message,一個target字段為空的Message。

2 同步屏障的工作原理

同步屏障隻在Looper死循環擷取待處理消息時才會起作用,也就是說同步屏障在MessageQueue.next函數中發揮着作用。

next函數我們在 Handler工作原理源碼解析 中曾經分析過,隻不過由于該機制并不影響Handler整體工作流程是以沒有展開講,下面我将相關代碼重新貼出來

Message next() 

        .....//省略
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        // 1.如果nextPollTimeoutMillis=-1,一直阻塞不會逾時。
        // 2.如果nextPollTimeoutMillis=0,不會阻塞,立即傳回。
        // 3.如果nextPollTimeoutMillis>0,最長阻塞nextPollTimeoutMillis毫秒(逾時)
        //   如果期間有程式喚醒會立即傳回。
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                //擷取系統開機到現在的時間
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages; //目前連結清單的頭結點
                //如果target==null,那麼它就是屏障,需要循環周遊,一直往後找到第一個異步的消息

                if (msg != null && msg.target == null) {
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    //如果有消息需要處理,先判斷時間有沒有到,如果沒到的話設定一下阻塞時間,
                    //場景如常用的postDelay
                    if (now < msg.when) {
                       //計算出離執行時間還有多久指派給nextPollTimeoutMillis,
                       //表示nativePollOnce方法要等待nextPollTimeoutMillis時長後傳回
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // 擷取到消息
                        mBlocked = false;
                       //連結清單操作,擷取msg并且删除該節點 
                        if (prevMsg != null) 
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        //傳回拿到的消息
                        return msg;
                    }
                } else {
                    //沒有消息,nextPollTimeoutMillis複位
                    nextPollTimeoutMillis = -1;
                }
                .....//省略

    }
           

從上面可以看出,

if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
           

其實通過代碼,當出現屏障的時候,會濾過同步消息,而是直接擷取其中的異步消息并傳回。如下圖所示:

Handler sync barrier(同步屏障)

當消息隊列開啟同步屏障的時候(即辨別為

msg.target == null

),消息機制會通過循環周遊,優先處理異步消息。這樣,同步屏障就起到了一種過濾和優先級的作用。

下面用示意圖簡單說明:

Handler sync barrier(同步屏障)

如上圖所示,在消息隊列中有同步消息和異步消息(黃色部分)以及一道牆----同步屏障(紅色部分)。有了記憶體屏障的存在,msg_2這個異步消息可以被處理,而後面的 msg_3等同步消息不會被處理。那麼什麼時候這些同步消息可以被處理呢?那就需要移除這個記憶體屏障,調用removeSyncBarrier()即可。

舉個栗子。開演唱會的時候,觀衆們都在體育館門口排隊依次等候檢票入場(相當于消息隊列中的普通消息),這個時候有一大波從業人員來了(相當于異步消息,優先級高于觀衆),如果他們出示工作證(不出示工作證,就相當于普通觀衆入場,也還是需要排隊,這種情形就是最前面所說的僅僅設定了msg.setAsynchronous(true)),保安立馬攔住(出示工作證就攔住就相當于開啟了同步屏障)進場的觀衆,先讓從業人員進去(隻處理異步消息,而過濾掉同步消息)。等從業人員全部進去了,保安不再阻攔觀衆(即移除記憶體屏障),這樣觀衆又可以進場了。隻要保安不解除攔截,那麼後面的觀衆就永遠不可能進場(不移除記憶體屏障,同步消息就不會得到處理)。

MessageQueue取出消息整體流程

MessageQueue取消息的流程畫了一個簡單的流程圖

Handler sync barrier(同步屏障)

總結

  • Handler在發消息時,MessageQueue已經對消息按照了等待時間進行了排序。
  • MessageQueue不僅包含了Java層消息機制同時包含Native消息機制
  • Handler消息分為異步消息與同步消息兩種。
  • MessageQueue中存在**“屏障消息“**的概念,當出現屏障消息時,會執行最近的異步消息,同步消息會被過濾。
  • MessageQueue在執行完消息隊列中的消息等待更多消息時,會處理一些空閑任務,如GC操作等。

3 如何發送異步消息

通常我們使用Handler發消息時,這些消息都是同步消息,如果我們想發送異步消息,那麼在建立Handler時使用以下構造函數中的其中一種(async傳true)

public Handler(boolean async);
public Handler(Callback callback, boolean async);
public Handler(Looper looper, Callback callback, boolean async);      

然後通過該Handler發送的所有消息都會變成異步消息

4 同步屏障的應用

Android應用架構中為了更快的響應UI重新整理事件在ViewRootImpl.scheduleTraversals中使用了同步屏障

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //設定同步障礙,確定mTraversalRunnable優先被執行
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //内部通過Handler發送了一個異步消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
           

mTraversalRunnable調用了performTraversals執行measure、layout、draw

為了讓mTraversalRunnable盡快被執行,在發消息之前調用MessageQueue.postSyncBarrier設定了同步屏障

postCallback()

最終走到了

Choreographer 的 postCallbackDelayedInternal()

private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
            //delayMillis傳的是0,故此處進入條件
            if (dueTime <= now) {
               //實際上單單從這個方法的名字我們就能意識到做的是跟幀有關的工作。
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }
           

最後,當要移除記憶體屏障的時候需要調用ViewRootImpl#unscheduleTraversals()。

void unscheduleTraversals() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //移除記憶體屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
           

 5、實戰

1、當點選同步消息會發送一個延時1秒執行普通消息,執行的結果列印log。

Handler sync barrier(同步屏障)

2、同步屏障會擋住同步消息。通過點選發送同步屏障->發送同步消息->移除同步消息測試

Handler sync barrier(同步屏障)

3、當點選發送同步屏障,會擋住同步消息,但是不會擋住異步消息。通過點選插入同步屏障->插入同步消息->插入異步消息->移除同步屏障 來測試(需要注意不要通過彈土司來測試,通過列印log。不然看不出效果)

Handler sync barrier(同步屏障)

測試代碼如下(省略布局檔案):

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Handler handler;
    private int token;

    public static final int MESSAGE_TYPE_SYNC=1;
    public static final int MESSAGE_TYPE_ASYN=2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initHandler();
        initListener();
    }

    private void initHandler() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                handler=new Handler(){
                    @Override
                    public void handleMessage(Message msg) {
                        if (msg.what == MESSAGE_TYPE_SYNC){
                            Log.d("MainActivity","收到普通消息");
                        }else if (msg.what == MESSAGE_TYPE_ASYN){
                            Log.d("MainActivity","收到異步消息");
                        }
                    }
                };
                Looper.loop();
            }
        }).start();
    }

    private void initListener() {
        findViewById(R.id.btn_postSyncBarrier).setOnClickListener(this);
        findViewById(R.id.btn_removeSyncBarrier).setOnClickListener(this);
        findViewById(R.id.btn_postSyncMessage).setOnClickListener(this);
        findViewById(R.id.btn_postAsynMessage).setOnClickListener(this);
    }

    //往消息隊列插入同步屏障
    @RequiresApi(api = Build.VERSION_CODES.M)
    public void sendSyncBarrier(){
        try {
            Log.d("MainActivity","插入同步屏障");
            MessageQueue queue=handler.getLooper().getQueue();
            Method method=MessageQueue.class.getDeclaredMethod("postSyncBarrier");
            method.setAccessible(true);
            token= (int) method.invoke(queue);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //移除屏障
    @RequiresApi(api = Build.VERSION_CODES.M)
    public void removeSyncBarrier(){
        try {
            Log.d("MainActivity","移除屏障");
            MessageQueue queue=handler.getLooper().getQueue();
            Method method=MessageQueue.class.getDeclaredMethod("removeSyncBarrier",int.class);
            method.setAccessible(true);
            method.invoke(queue,token);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //往消息隊列插入普通消息
    public void sendSyncMessage(){
        Log.d("MainActivity","插入普通消息");
        Message message= Message.obtain();
        message.what=MESSAGE_TYPE_SYNC;
        handler.sendMessageDelayed(message,1000);
    }

    //往消息隊列插入異步消息
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
    private void sendAsynMessage() {
        Log.d("MainActivity","插入異步消息");
        Message message=Message.obtain();
        message.what=MESSAGE_TYPE_ASYN;
        message.setAsynchronous(true);
        handler.sendMessageDelayed(message,1000);
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    public void onClick(View v) {
        int id=v.getId();
        if (id == R.id.btn_postSyncBarrier) {
            sendSyncBarrier();
        }else if (id == R.id.btn_removeSyncBarrier) {
            removeSyncBarrier();
        }else if (id == R.id.btn_postSyncMessage) {
            sendSyncMessage();
        }else if (id == R.id.btn_postAsynMessage){
            sendAsynMessage();
        }
    }
    
}
           

總結

當我們調用mHandler.getLooper().getQueue().postSyncBarrier()時,target 即為 null ,也就開啟了同步屏障。當消息隊列 MessageQueue 處理消息時,如若開啟了記憶體屏障,會過濾同步消息而優先循環處理其中的異步消息。