Android消息机制之同步屏障

1、Message

Android中的Message分为三种:

  • 同步消息
  • 异步消息
  • 同步屏障消息

它们都是Message,只是成员变量有些区别。

一般我们通过 Handler 发送消息(如调用Handler.sendMessage(@NonNull Message msg)等),最终都会调用 Handler.enqueueMessage()让消息入队,如下:

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

这里msg.target 会被赋值为this, 而 this 即为我们的 Handler 对象,并且Message的成员变量 mAsynchronous 默认为 false,这种消息是同步消息,我们一般发送的消息都是同步消息。

相对应也应该有异步消息吧?的确,还有一种很容易被忽略的 异步消息,因为除了系统源码外,我们一般很少会使用异步消息。那么,如何发送一个异步消息呢?

简单来说有两种方式。

一种是直接设置消息为异步的:

java 复制代码
Message msg = mHandler.obtainMessage();
msg.setAsynchronous(true);
mMyHandler.sendMessage(msg);

还有一种需要用到 Handler 的构造方法,不过该方法已被标记为@hide了,普通应用无法使用:

java 复制代码
  public Handler(boolean async) {
     this(null, async);
  }

但是如果没有同步屏障,异步消息与同步消息的执行并没有什么区别。

2、同步屏障

同步屏障究竟有什么作用?

同步屏障为handler消息机制提供了一种优先级策略,让异步消息的优先级高于同步消息。如何开启同步屏障呢?通过MessageQueue.postSyncBarrie()可以开启同步屏障:

java 复制代码
    /**
     * Posts a synchronization barrier to the Looper's message queue.
     *
     * Message processing occurs as usual until the message queue encounters the
     * synchronization barrier that has been posted.  When the barrier is encountered,
     * later synchronous messages in the queue are stalled (prevented from being executed)
     * until the barrier is released by calling {@link #removeSyncBarrier} and specifying
     * the token that identifies the synchronization barrier.
     *
     * This method is used to immediately postpone execution of all subsequently posted
     * synchronous messages until a condition is met that releases the barrier.
     * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier
     * and continue to be processed as usual.
     *
     * This call must be always matched by a call to {@link #removeSyncBarrier} with
     * the same token to ensure that the message queue resumes normal operation.
     * Otherwise the application will probably hang!
     *
     * @return A token that uniquely identifies the barrier.  This token must be
     * passed to {@link #removeSyncBarrier} to release the barrier.
     *
     * @hide
     */
    @UnsupportedAppUsage
    @TestApi
    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

postSyncBarrier()方法前面有一大段注释,大概意思是该方法会往MessageQueue中插入一条同步屏障message,遇到该message后,MessageQueue中的同步消息会延迟执行,直到通过调用removeSyncBarrier()方法移除同步屏障;而异步消息有豁免权,可以正常执行。

java 复制代码
    // The next barrier token.
    // Barriers are indicated by messages with a null target whose arg1 field carries the token.
    //这里有解释:同步屏障是一个target为null并且arg1为这个token的消息
    @UnsupportedAppUsage
    private int mNextBarrierToken;
  
    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) {
            final int token = mNextBarrierToken++; //为token赋值
            final Message msg = Message.obtain();
            msg.markInUse();

            //这里初始化Message对象的时候没有给target赋值, 即target==null
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;

            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            //将msg插入消息队列
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

可以看到,Message 对象初始化的时候并没有给 target 赋值,因此同步屏障消息的target == null

那么插入同步屏障消息后,异步消息是如何被优先处理的呢?

如果对消息机制有所了解的话,应该知道消息的最终处理是在消息轮询器Looper.loop()中,而loop()循环中会调用MessageQueue.next()从消息队列中取消息,来看看关键代码:

java 复制代码
@UnsupportedAppUsage
Message next() {
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                Message msg = mMessages;
                //如果msg.target为null,是一个同步屏障消息,则进入这个判断
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    //先执行do,再执行while,遇到同步消息会跳过,遇到异步消息退出循环
                    //即取出的msg为该屏障消息后的第一条异步消息,屏障消息不会被取出
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
              
                if (msg != null) {
                    //如果消息的处理时间大于当前时间则等待
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        //处理消息
                        mBlocked = false;
                        
                        if (prevMsg != null) {
                        	//将消息移除
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        //返回消息
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
                ...
    }

从上面的代码可以看出,当执行到同步屏障消息(即target == null的Message)时,消息机制优先处理异步消息。由于代码中先执行do再执行while,取出的msg为该屏障消息后的第一条异步消息,而屏障消息不会被取出

下面用示意图简单说明:

如上图所示,在消息队列中有同步消息和异步消息(黄色部分)以及同步屏障消息(红色部分)。当执行到同步屏障消息的时候,msg_2 和 msg_M 这两个异步消息会被优先处理,而msg_3 等同步消息则要等异步消息处理完后再处理。

3、同步屏障的使用场景

在日常的应用开发中,我们很少会用到同步屏障。Android 系统源码中UI更新就是使用的同步屏障,这样会优先处理更新UI的消息,尽量避免造成界面卡顿。

UI更新都会调用ViewRootImpl.scheduleTraversals(),其代码如下:

java 复制代码
public final class ViewRootImpl{

  @UnsupportedAppUsage
  void scheduleTraversals() {
     if (!mTraversalScheduled) {
         mTraversalScheduled = true;
         //往队列中插入同步屏障消息
         mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
         //往队列中插入异步消息
         mChoreographer.postCallback(
                 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         if (!mUnbufferedInputDispatch) {
             scheduleConsumeBatchedInput();
         }
         notifyRendererOfFramePending();
         pokeDrawLockIfNeeded();
     }
  }
  
}

Choreographer.postCallback()最终调用了postCallbackDelayedInternal()方法:

java 复制代码
public final class Choreographer {

	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);
	
	            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); 
	            }
	        }
	    }
}

这里就往队列中插入了同步屏障消息,然后又插入了异步消息,UI 更新相关的消息就可以优先得到处理。

前面代码中我们看到,同步屏障消息并不会自己移除,所以需要调用相关代码来移除同步屏障消息,让同步消息可以正常执行。Android源码中在执行绘制流程之前执行了移除同步屏障的代码:

java 复制代码
public final class ViewRootImpl{
   void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //移除同步屏障消息
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
			...
			//里面执行了绘制的3大流程
            performTraversals();
			...			
        }
    }
}

public final class MessageQueue {
    /**
     * Removes a synchronization barrier.
     *
     * @param token The synchronization barrier token that was returned by
     * {@link #postSyncBarrier}.
     *
     * @throws IllegalStateException if the barrier was not found.
     *
     * @hide
     */
    @UnsupportedAppUsage
    @TestApi
    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;
            //找到同步屏障消息p
            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) {
            	//prev的next指向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);
            }
        }
     }
   } 
}

通过以上的分析,对于同步屏障的原理已经了解了吧。在绘制流程中使用同步屏障,保证了在VSYNC信号到来时,绘制任务可以及时执行,避免造成界面卡顿。

相关推荐
喵叔哟23 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django