4.7理解Handler同步屏障
目录介绍
- 01.先了解一个场景
- 1.1 handler消息类型
- 1.2 什么叫同步屏障
- 1.3 为何要同步屏障
- 02.Android中异步消息
- 2.1 如何发异步消息
- 2.2 如何设置同步屏障
- 2.3 同步屏障应用场景
- 2.4 同步屏障的代价
- 2.5 同步屏障使用流程
- 03.同步屏障源码解读
- 3.1 postSyncBarrier开启同步屏障
- 3.2 如何判断同步屏障
- 3.3 屏障消息工作原理
- 3.4 如何处理有同步和异步混合消息
01.先了解一个场景
1.1 handler消息类型
- Handler的Message种类分为三种:
- 普通消息
- 异步消息
- 屏障消息
- 普通消息
- 普通消息又称为同步消息,我们平时发的消息基本都是同步消息。
- 异步消息
- Handler的构造方法有个async参数,默认的构造方法此参数是false,只要我们在构造handler对象的时候,把该参数设置为true就可以。
- 在创建Message对象时,直接调用Message的setAsynchronous()方法。
- 普通消息和异步消息区别
- 在一般情况下,异步消息和同步消息没有什么区别,但是一旦开启了同步屏障以后就有区别了。
1.2 什么叫同步屏障
- 什么场景用到同步屏障
- 在系统源码中就有使用。Android系统中的UI更新相关的消息即为异步消息,需要优先处理。那么handler机制中如何优先消息了,这个就需要了解同步屏障。
- 屏幕刷新机制
- 16ms左右刷新UI,而是60hz的屏幕,即1s刷新60次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算。具体到我们的代码中,可以认为就是执行onMeasure()、onLayout()、onDraw()这些方法。
- 消息机制的同步屏障
- 同步屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。
- 不过异步消息却例外,屏障不会挡住异步消息,因此可以认为,屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。
- 一句话概括
- 其实就是阻碍同步消息,只让异步消息通过。
1.3 为何要同步屏障
- 如果设置了异步消息,则首先让先执行。可以理解为先执行异步消息,然后在执行同步消息(开发中一般使用handler发送消息都是同步消息)。
02.Android中异步消息
2.1 如何发异步消息
- 在Android中什么是异步消息?即给:
Message.setAsynchronous(true);
2.2 如何设置同步屏障
- 开启同步屏障的方法就是调用下面的方法:MessageQueue#postSyncBarrier()
2.3 同步屏障应用场景
- view 绘制原理
- view绘制的起点是在 viewRootImpl.requestLayout() 方法开始,这个方法会去执行上面的三大绘制任务,就是测量布局绘制。
- 同步屏障应用场景
- 调用requestLayout()方法之后,并不会马上开始进行绘制任务,而是会给主线程设置一个同步屏障,并设置 ASYNC 信号监听。
- 当 ASYNC 信号的到来,会发送一个异步消息到主线程Handler,执行我们上一步设置的绘制监听任务,并移除同步屏障。
- 为何要设置同步屏障
- 在等待ASYNC信号的时候主线程什么事都没干?是的。这样的好处是:保证在ASYNC信号到来之时,绘制任务可以被及时执行,不会造成界面卡顿。
2.4 同步屏障的代价
- 同步屏障的代价
- 同步消息最多可能被延迟一帧的时间,也就是16ms,才会被执行。
- 主线程Looper造成过大的压力,在VSYNC信号到来之时,才集中处理所有消息。
- 改善这个问题办法就是:使用异步消息。
- 发送异步消息到MessageQueue中时,在等待VSYNC期间也可以执行我们的任务,让我们设置的任务可以更快得被执行且减少主线程Looper的压力。
- 异步消息机制本身就是为了避免界面卡顿,那我们直接使用异步消息,会不会有隐患?
- 什么情况的异步消息会造成界面卡顿:异步消息任务执行过长、异步消息海量。若异步消息海量到达影响界面绘制,那么即使是同步任务,也是会导致界面卡顿的;原因是MessageQueue是一个链表结构,海量的消息会导致遍历速度下降,也会影响异步消息的执行效率。
- 所以我们应该注意的一点是:不可在主线程执行重量级任务,无论异步还是同步。
2.5 哪里用同步屏障
- 比如,在View更新时,draw、requestLayout、invalidate等很多地方都调用了。ViewRootImpl#scheduleTraversals()。
@UnsupportedAppUsage void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; // 开启同步屏障 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 发送异步消息 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
- 在这里,mChoreographer.postCallback最终会执行到了Choreographer#postCallbackDelayedInternal()
- 代码流程:postCallback--->postCallbackDelayed--->postCallbackDelayedInternal
- 可以看到,这里就开启了同步屏障,并且发送了异步消息。由于UI相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。
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); } } }
- 当然要移除同步屏障的时候,调用ViewRootImpl#unscheduleTraversals
void unscheduleTraversals() { if (mTraversalScheduled) { mTraversalScheduled = false; // 移除同步屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); mChoreographer.removeCallbacks( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); } }
- 在ViewRootImpl中的doTraversal()方法中也会移除同步屏障,这里移除是因为requestLayout或者invalidate的时候,刷新之后,在doTraversal()中就会移除同步屏障,因为此时消息已经发送并且处理了。
03.同步屏障源码解读
3.1 postSyncBarrier开启同步屏障
- 首先看一下,MessageQueue#postSyncBarrier(),源码如下所示
- 可以看到,Message对象初始化的时候并没有给target赋值,因此target==null的来源就找得到了。
- 这样就可以插入一条target==null的消息,这个消息就是一个同步屏障。
@TestApi public int postSyncBarrier() { // 这里传入的时间是从开机到现在的时间戳 return postSyncBarrier(SystemClock.uptimeMillis()); } /** * 这就是创建的同步屏障的方法 * 同步屏障就是一个同步消息,只不过这个消息的target为null */ 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++; // 从消息池中获取Message final Message msg = Message.obtain(); msg.markInUse(); // 初始化Message对象的时候,并没有给Message.target赋值, // 因此Message.target==null msg.when = when; msg.arg1 = token; Message prev = null; Message p = mMessages; if (when != 0) { // 这里的when是要加入的Message的时间 // 这里遍历是找到Message要加入的位置 while (p != null && p.when <= when) { // 如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步 // 消息里有时间小于T,则prev也不为null prev = p; p = p.next; } } // 根据prev是否为null,将msg按照时间顺序插入到消息队列的合适位置 if (prev != null) { // invariant: p == prev.next msg.next = p; prev.next = msg; } else { msg.next = p; mMessages = msg; } return token; } }
3.2 如何判断同步屏障
- 通常通过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。
3.3 屏障消息工作原理
- 消息的最终处理其实都是在消息轮询器Looper#loop()中,
- 而loop()循环中会调用MessageQueue#next()从消息队列中进行取消息。
- 然后看MessageQueue.next方法
- 可以看出,当消息队列开启同步屏障的时候(即标识为msg.target==null),消息机制在处理消息的时候,会优先处理异步消息。
- 这样,同步屏障就起到了一种过滤(只会处理异步消息)和优先级(异步消息的优先级要高于同步消息)的作用。
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的消息 // 如果target==null,那么它就是屏障,需要循环遍历, // 一直往后找到第一个异步消息,即msg.isAsynchronous()为true // 这个target==null的消息,不会被取出处理,一直会存在 // 每次处理异步消息的时候,都会从头开始轮询 // 都需要经历从msg.target开始的遍历 if (msg != null && msg.target == null) { // 使用一个do..while循环 // 轮询消息队列里的消息,这里使用do..while循环的原因 // 是因为do..while循环中取出的这第一个消息是target==null的消息 // 这个消息是同步屏障的标志消息 // 接下去进行遍历循环取出Message.isAsynchronous()为true的消息 // isAsynchronous()为true就是异步消息 do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { // 如果有消息需要处理,先判断时间有没有到,如果没有到的话设置阻塞时间 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; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); // 返回拿到的消息 return msg; } } else { // 没有消息,nextPollTimeoutMillis复位 nextPollTimeoutMillis = -1; } ... } } }
3.3 如何处理有同步和异步混合消息
- 先看一张图
image
- 在消息队列中有同步消息和异步消息(黄色部分)以及一道墙(同步屏障--红色部分)。
- 有了同步屏障的存在,msg_2和msg_M这两个异步消息可以被优先处理,而后面的msg_3等同步消息则不会被处理。
- 那么这些同步消息什么时候可以被处理呢?
- 需要先移除这个同步屏障,即调用MessageQueue#removeSyncBarrier()
@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; 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.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); } } }
04.最后总结一下
- ViewRootImpl 将遍历view树包装成一个Runnable并抛到Choreographer, 在抛之前会向主线程消息队列中抛同步屏障
- 同步屏障也是一个Message,只不过 target 等于null
- 取下一条message的算法中,若遇到同步屏障,则会越过同步消息,向后遍历找第一条异步消息找到则返回(Choreographer抛的异步消息),若没有找到则会执行epoll挂起
- 当执行到遍历View树的 runnable时,ViewRootImpl会移除同步屏障