https://juejin.cn/post/6954273327133229064https://juejin.cn/post/7196549577162965029https://juejin.cn/post/6971981915934949390
产生背景
Handler发送的Message会存放到MessageQueue中,MessageQueue维护者一个优先级队列。这个优先级队列按照时间大小升序,Looper每次则从取出队首的Message进行分发。有没有可能指定队列中的Message被优先取出分发处理呢? 当然是有的,同步屏障机制应运而生!
什么是同步屏障
首先我们需要明白Message的分类,其实是有三种的:
- 同步消息
- 异步消息
- 同步屏障消息
这里的同步和异步并不是我们在多线程中的概念,暂时可以把它就看作是一种标识,仅用来区分不同的消息。 通常我们构造的Handler,使用send和post发送的消息都属于同步消息,这些消息会进入MessageQueue中按时间优先级排队。暂且不介绍异步消息。 同步屏障就是在消息队列中插入一个屏障,插入了屏障之后,所有的同步消息会被屏蔽,不能被执行,但是异步消息却不受影响,可以继续执行。 可以用一个简单的比喻,我们把同步消息看作是普通人,消息队列是一条长长的队伍,而异步消息是VIP顾客,同步屏障就是队伍中拉起的一条警戒线。当在队伍中拉起了一条警戒线,那些普通人就被隔离在警戒线外,而VIP顾客可以正常的排队被服务。
如何区分同步消息和异步消息
经过上面的介绍,我们对同步消息其实并不陌生。我们一直在使用的消息就是同步消息,或者说是普通消息。 在我们使用handler发送一条消息时,调用:
handler.sendMessage(handler.obtainMessage());
最终调用到的是Handler类中的enqueueMessage
方法,因为Handler内部自动帮我们设置mAsynchronous
字段为false,所以每次我们发送的消息其实都是同步消息。
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
//注意这里,我们对target字段赋值
msg.target = this;
//判断是否需要将消息设置为异步消息
//mAsynchronous字段默认设置为false
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
再深入看,首先会对target判空,因为target是负责发送和处理这条消息的Handler的引用。如果它为空,那么这条消息就不知道要发送给哪一个Handler处理。
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
...
// 如果需要唤醒,则唤醒
//因此插入同步(异步)消息时会唤醒消息队列
//调用的是nativeWake
if (needWake) {
nativeWake(mPtr);
}
}
而发送异步消息,就需要我们手动设置(具体看下文)
如何区分同步屏障消息
虽然很好理解同步屏障消息,那对应到代码上Handler又是如何区分的呢?这道屏障是什么? 详看代码,我们可以通过调用postSyncBarrier
方法,MessageQueue类的内部会去帮我们插入一条msg到消息队列中,这条消息的特点是它的target字段为空,因为这就是区分同步屏障消息的特征点。
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
synchronized (this) {
//获取一个token,用来撤销屏障消息
final int token = mNextBarrierToken++;
//msg的target属性为空,与上面的enqueueMessage方法中赋值为this不同
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
//根据时间将同步屏障消息插入到消息队列中
while (p != null && p.when z 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;
}
//返回token,作用是后面需要用来撤销屏障消息
return token;
}
}
总结:
- 屏障消息和同步(异步)消息的区别是屏障消息的target属性为空,target属性是负责发送和处理这条消息的Handler的引用。
- 屏障消息在
postSyncBarrier
方法中被创建,会根据时间被插入到消息队列中,屏障消息之后的同步消息都会被屏蔽 postSyncBarrier
方法返回一个token值,这个整型数值用来撤销屏障消息- 往队列中差插入不同消息会唤醒消息队列,但是插入屏障不会。
如何发送异步消息
通过上面的分析,我们直到只需要将mAsynchronous
字段默认设置为true即可。在构造Handler时,系统也提供了相应的构造函数。
//1.方式一,也是常用的一种
public Handler(boolean async) {
this(null, async);
}
//2.方式二
public Handler(@NonNull Looper looper, @Nullable Callback callback) {
this(looper, callback, false);
}
//3.方式三
public Handler(@Nullable Callback callback, boolean async) {
...
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
除此之外,Message类中还提供了API,供我们直接设置mAsynchronous
字段的值。
//4.方式四
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
消息的处理过程
MessageQueue是通过next方法来遍历消息的。 处理消息时首先会进入一个死循环,其中有一个标识字段nextPollTimeoutMillis用于控制队列是否需要阻塞,阻塞的具体实现由native层的函数nativePollOnce实现,然后从消息队列(实现的数据结构是单链表)中取下一条消息,根据target字段是否为空来判断是否是屏障消息,如果是屏障消息就往后检索isAsynchronous标识为true的消息,也就是异步消息。然后真正的处理消息。可见这就是为什么设置了屏障后,会屏蔽同步消息只处理异步消息的原因。 同时我们我证实了同步消息和异步消息其实没有本质的不同,只是在设置了屏障时为了做区分而引入不同的概念,真正的处理msg的逻辑(代码中标号8及之后的逻辑),并没有区分同步和异步。
Message next() {
······
int pendingIdleHandlerCount = -1; // -1 only during first iteration
//1.判断队列中是否有消息的标识字段,是nativePollOnce方法的入参
int nextPollTimeoutMillis = 0;
//2.进入死循环
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//3.判断是否需要阻塞循环,让CPU进入休眠状态
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
//4.从单链表中检索出下一条消息
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//5.如果此消息是一个屏障消息
if (msg != null && msg.target == null) {
//6.通过do循环查找到最近的一条异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//7.找到了消息,对消息做处理
//注意这里之后的逻辑就不区分异步和同步消息了,都做同样的处理
//因为如果有消息屏障,上面的do循环会帮助我们找到最近的异步消息
//如果没有消息屏障,那么msg就是最近的那一条消息
if (msg != null) {
//8.如果消息的处理时间小于当前时间,说明还没到消息的处理时间,则等待
//计算出需要等待的时间再唤醒消息队列
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//9.真正处理消息
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 {
//10.队列中没有找到消息则进入阻塞状态,设置标识为-1,初始值为0
//nativePollOnce执行具体的判断逻辑,根据标识的值决定是否阻塞
nextPollTimeoutMillis = -1;
}
······
}
for (int i = 0; i < pendingIdleHandlerCount; i++) {
······
}
······
}
}
总结:同步和异步消息并没有本质的区别,只是为了在设置屏障时区分二者,让异步消息能优先于同步消息被处理,所以Handler的同步消息屏障是一种优先级策略。
同步消息的移除
之前的分析中提到,在设置同步消息屏障时返回 了一个token。因为设置了同步屏障,如果不主动撤销,那么同步屏障之后的同步消息都将不会被处理,所以在我们处理完需要优先处理的异步消息之后,就需要手动撤销屏障。 通过调用removeSyncBarrier
方法即可,其中的token就是在设置屏障时的返回值。
public void removeSyncBarrier(int token){}
同步屏障在绘制流程中的应用
在Android的绘制流程中,ViewRootImpl类中有一个方法requestLayout
ViewRootImpl类是系统中每个界面上的 View 的刷新,绘制,点击事件的分发的发起者
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//校验主线程
checkThread();
mLayoutRequested = true;
//调用这个方法启动绘制流程
scheduleTraversals();
}
}
首先会检验发起布局请求的线程是否为主线程,具体校验方式是比较ViewRootImpl构造记录的mThread和当前线程是否一致),之后在scheduleTraversals
方法中启动绘制流程,并调用postSyncBarrier
添加同步屏障。
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息
//2. 记录了postSyncBarrier的返回值token,用于之后移除屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//3.将mTraversalRunnable保存到Choreographer中
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
...
//在doTraversal方法中移除同步消息屏障
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
}
}
关于绘制的过程可以讲很多很深,这里主要涉及到三个重要的信息,暂且知其大概:
- mTraversalRunnable
- mChoreographer
- 设置同步消息屏障
- 首先看
mTraversalRunnable
,它的作用就是从ViewRootImpl 从上往下执行performMeasure
、performLayout
、performDraw
。也就是我们常言道的测量、布局、绘制三个流程 - Choreographer主要是为了配合Vsync信号,给上层app的渲染提供一个稳定的Message处理时机,也就是Vsync信号到来时,系统通过对Vsync信号的调整,来控制每一帧绘制操作的时机。当Vsync信号到来时,会往主线程的MessageQueue中插入一条异步消息,由于在
scheduleTraversals
中给MessageQueue中插入了同步屏障消息,那么当执行到同步屏障时,会取出异步消息执行。
具体来看看在Choreographer中插入异步消息的实现:
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
...
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);
}
}
}
同步消息屏障,为我们系统的绘制流程的优先级提供了实现基础,保障每一条绘制消息都是能够优先被系统处理。