看同步屏障之前首先要对Handler有一定的了解,可以先看这篇文章:
Handler又是什么鬼东西(附源码)
定义
大家应该知道,线程的消息都是放到同一个MessageQueue里面,取消息的时候是互斥取消息,而且只能从头部取消息,而添加消息是按照消息的执行的先后顺序进行的排序。那么问题来了,同一个时间范围内的消息,如果它是需要立刻执行的,那我们应该怎么办,按照常规的办法,我们需要等到队列轮询到我自己的时候才能执行,但是显然达不到我们立即执行的需求,所以,我们需要给紧急需要执行的消息创建一个绿色通道,这个绿色通道就是同步屏障的概念。
顾名思义,同步屏障就是给同步消息设置屏障,让异步消息(需要加急处理得消息)优先执行。消息分为同步消息(也就是普通消息)
、异步消息
和屏障消息
同步消息:我们平常正常使用,发送得消息其实就是同步消息,在enqueueMessage
中将handler赋值给了msg的target,msg.target=handler
异步消息:设置了async
异步消息标志的消息
屏障消息:target==null
的消息就是屏障消息
1. 同步屏障如何开启
我们可以在MessageQueue
的postSyncBarrier()
方法中开启同步屏障,源码如下:
// MessageQueue.java
/**
* @hide
*/
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
//就是这里!!!初始化Message对象的时候,并没有给target赋值,因此 target==null
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
// 把当前需要执行的Message全部执行
if (when != 0) {
//如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步消息里有时间小于T,则prev也不为null
while (p != null && p.when <= when) {
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;
}
}
在上面的源码中我们可以看到,Message 对象初始化的时候并没有给 target 赋值,因此当前消息的target==null
,也就是往消息队列中插入了一个屏障消息。
2. 开启同步屏障后,异步消息是怎么处理的呢?
如果对消息机制有所了解的话,应该知道消息的最终处理是在消息轮询器Looper.loop()
中,而loop()循环中会调用MessageQueue.next()
从消息队列中取消息,来看看关键代码
// MessageQueue.java
Message next() {
...
// 阻塞时间
// 1.如果nextPollTimeoutMillis=-1,一直阻塞不会超时。
// 2.如果nextPollTimeoutMillis=0,不会阻塞,立即返回。
// 3.如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时)
// 如果期间有程序唤醒会立即返回。
int nextPollTimeoutMillis = 0;
for (;;) {
...
// 阻塞对应时间
nativePollOnce(ptr, nextPollTimeoutMillis);
// 对MessageQueue进行加锁,保证线程安全
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//关键!!!
//如果target==null,那么它就是屏障,需要循环遍历,一直往后找到第一个异步的消息
if (msg != null && msg.target == null) {
// 同步屏障,找到下一个异步消息
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 {
// 获得消息且现在要执行,标记MessageQueue为非阻塞
mBlocked = false;
// 一般只有异步消息才会从中间拿走消息,同步消息都是从链表头获取
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
// 没有消息,进入阻塞状态
nextPollTimeoutMillis = -1;
}
// 当调用Looper.quitSafely()时候执行完所有的消息后就会退出
if (mQuitting) {
dispose();
return null;
}
...
}
...
}
}
从上面可以看出,当消息队列开启同步屏障的时候(即标识为msg.target == null
),那么会循环遍历整个链表找到标记为异步消息的Message,即isAsynchronous
返回true
,其他的消息会直接忽视,那么异步消息就会提前被执行了。这样,同步屏障就起到了一种过滤和优先级的作用。
如下图所示,屏障消息插入后,优先处理异步消息:
注意,同步屏障不会自动移除,使用完成之后需要手动进行移除,即调用removeSyncBarrier()
,不然会造成同步消息无法被处理。从源码中可以看到如果不移除同步屏障,那么他会一直在那里,这样同步消息就永远无法被执行了。
唤醒:有了同步屏障,那么唤醒的判断条件就必须再加一个:MessageQueue中有同步屏障且处于阻塞中,此时在所有异步消息前插入新的异步消息。这个也很好理解,跟同步消息是一样的。如果把所有的同步消息先忽视,就是插入新的链表头且队列处于阻塞状态,这个时候就需要被唤醒了。
再来看一下唤醒模块代码:
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
...
// 对MessageQueue进行加锁
synchronized (this) {
...
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 1. 当线程被阻塞,且目前有同步屏障,且入队的消息是异步消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
// 2. 如果找到一个异步消息,说明前面有延迟的异步消息需要被处理,不需要被唤醒
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
}
// 如果需要则唤醒队列
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
注释1处表明,如果当前处于阻塞状态,有同步屏障,并且插入的是一个异步消息,那么就需要唤醒。
注释2处表示,如果当前插入的异步消息的位置不是所有异步消息之前,那么不需要唤醒。
3. 如何发送一个异步消息
发送异步消息有两种办法:
- 使用异步类型的Handler发送的全部Message都是异步的
- 给Message标志异步
Handler有一系列带Boolean类型的参数的构造器,这个参数就是决定是否是异步Handler:
// Handler.java
/**
*
* @hide
*/
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
// 这里赋值
mAsynchronous = async;
}
但是异步类型的Handler构造器是标记为hide,我们无法使用,所以我们使用异步消息只有通过给Message设置异步标志:
// Handler.java
// 源码
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
// 代码中设置
Message msg = mMyHandler.obtainMessage();
msg.setAsynchronous(true);
mMyHandler.sendMessage(msg);
4. 同步屏障应用场景
在我们日常的开发中,其实很少用到同步屏障,那么系统中什么时候会用到同步屏障呢?Android 系统中的 UI 更新相关的消息即为异步消息,每16ms刷新一次页面,需要优先处理。
比如,在 View 更新时,draw、requestLayout、invalidate 等很多地方都调用了ViewRootImpl#scheduleTraversals()
,如下
//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//开启同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//发送异步消息
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
先通过mHandler.getLooper().getQueue().postSyncBarrier();
开启同步屏障,再通过mChoreographer.postCallback()
最终调用到Choreographer.postCallbackDelayedInternal()
,在注释处发送异步消息。
由于 UI 更新相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。
//Choreographer.java
private void postCallbackDelayedInternal(int callbackType,Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + 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);
}
}
}
5. 移除同步屏障
同步屏障既然有添加,肯定要有移除,否则同步消息永远无法执行,调用ViewRootImpl.unscheduleTraversals()
最终调用removeSyncBarrier()
移除同步屏障。
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
// MessageQueue.java
/**
* @hide
*/
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);
}
}
}
6. 小结
同步屏障就是屏蔽同步消息,让异步消息优先处理的逻辑。
在MessageQueue.enqueueMessage()
中可以看到,当msg.target == null
的时候,轮询遍历消息队列寻找异步消息。
所以开启同步屏障需要msg.target == null
,我们可以通过调用MessageQueue
的postSyncBarrier()
开启同步屏障。
发送异步消息需要为msg设置async,即调用msg的setAsynchronous(true)
设置消息为异步消息,这样就可以使用同步屏障了。
当处理完成异步消息后,记得关闭同步屏障,调用MessageQueue
的removeSyncBarrier()
,移除同步屏障,使同步消息正常运行。