MessageQueue中的同步屏障
Message,最早可以翻到两年前写的一篇粗浅博客《信件:Message》。当时还不明白异步消息、同步消息,更不懂今天要写的同步屏障。
同步屏障,简而言之就是让异步消息的优先级高于同步消息,优先被处理。
1、同步屏障
MessageQueue设置同步屏障后,会拦截对同步消息的处理,只会处理异步消息,如果没有异步消息则进入阻塞状态。若没有及时移除同步屏障,所有的同步消息都不会被处理,可能会导致ANR。
1.1、添加同步屏障 @hide
MessageQueue中的postSyncBarrier()方法添加一个同步屏障,同时返回一个int类型的数值,作为唯一标识此屏障的令牌(token)。
/**
* 向Looper中的MessageQueue发布一个同步屏障。
* @hide 隐藏方法,应用侧无法直接调用
*/
@UnsupportedAppUsage
@TestApi
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}使用SystemClock.uptimeMillis()当前系统启动时间作为参数调用另一个私有方法:postSyncBarrier(long when)。在Handler消息延迟原理一文中知道,Message是按照消息目标处理时间顺序递增排列在链表中。同样,在postSyncBarrier(long when)方法里面:
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) {
//获取同步屏障标志,用mNextBarrierToken自增。可见同步屏障可以设置不止一个。
final int token = mNextBarrierToken++;
//获取消息,作为添加同步屏障的屏障消息。此消息没有target,也就是Handler,因此无法被处理。
final Message msg = Message.obtain();
msg.markInUse();
//发送同步屏障的时间SystemClock.uptimeMillis(),就是屏障消息的目标处理时间
msg.when = when;
//保存同步屏障token标志
msg.arg1 = token;
//链表前面一个节点
Message prev = null;
//当前消息队列的链表头
Message p = mMessages;
if (when != 0) {
//遍历Message链表,根据when时间找到插入屏障消息的位置
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
//将消息屏障插入到Message链表中
msg.next = p;
prev.next = msg;
} else {
//当前Message前面已无消息,将当前屏障消息作为链表头
msg.next = p;
mMessages = msg;
}
return token;
}
}仔细阅读postSyncBarrier(long when)方法,可以看出:屏障消息和普通消息(异步/同步)的区别在于屏障没有tartget,也就是处理此Message的Handler。普通消息有target是因为它最终将要被分发给对应的target处理。而屏障消息不需要被分发,它是用来挡住同步消息来保证异步消息优先处理。
1.2、移除同步屏障 @hide
添加同步屏障后,一定要移除。使用removeSyncBarrier(int token)方法传入同步屏障对应的token即可撤销此同步屏障。
/**
* 移除同步障碍。
*
* @param token postSyncBarrier方法返回的同步屏障令牌。
*
* @throws IllegalStateException 如果没有发现对应的同步屏障,抛出异常。
*
* @hide 隐藏方法,应用侧无法直接调用
*/
@UnsupportedAppUsage
@TestApi
public void removeSyncBarrier(int token) {
// 从队列中删除同步屏障。
// 如果队列不再被屏障阻塞,则唤醒队列。
synchronized (this) {
//经典链表遍历
Message prev = null;
Message p = mMessages;
//遍历消息队列,直到找到消息队列中的arg1与token或到链表队尾
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
//如果没有token与之对应的同步屏障消息存在,抛出异常。
if (p == null) {
throw new IllegalStateException("The specified message queue synchronization "
+ " barrier token has not been posted or has already been removed.");
}
//是否需要唤醒Looper
final boolean needWake;
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
//回收Message,以被Message.obtain()重复使用
p.recycleUnchecked();
// 唤醒
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}1.3、同步屏障原理 next()
屏障和普通消息一样根据时间来插入到消息队列中的适当位置,拦截后面的同步消息的分发,只处理异步消息。在MessageQueue的next()方法中代码段如下:
Message next() {
...
for (;;) {
...
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());
}
//异步消息不为null
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;
}
}
...
}
}一旦给MessageQueue设置同步屏障,只会处理异步消息。可以理解同步屏障的作用,让异步消息优先级高于同步消息。
next()方法及Looper/MessageQueue消息机制在Handler、Looper、MessageQueue消息机制原理一文有详细介绍。
1.4、同步屏障场景
框架在ViewRootImpl的scheduleTraversals()方法中就用到了postSyncBarrier()设置同步屏障。为了更快的执行优先级高的遍历操作,避免被应用其它同步消息阻塞。
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//设置同步屏障,并保存此屏障令牌到mTraversalBarrier变量
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//向Choreographer注册CALLBACK_TRAVERSAL类型回调,下一个VSYNC信号到来时执行mTraversalRunnable任务
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}同步屏障的添加和移除是一对配对操作。设置同步屏障后,一定要移除。当scheduleTraversals()方法调用后,会在后续执行的doTraversal()中移除同步屏障:
//此方法在mTraversalRunnable的run()方法中被调用
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除mTraversalBarrier所标志的同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}又或是在成对的unscheduleTraversals()方法中移除同步屏障:
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}2、如何发送异步消息?
开发过程中,绝大多数场景发送的都是同步消息:
Handler handler = new Handler(Looper.getMainLooper());
Message message = Message.obtain();
//...
handler.sendMessage(message);那么如何发送异步消息呢?
2.1、设置异步消息
通过Message的setAsynchronous(boolean async)方法,将默认的同步Message设置成异步Message。
/** If set message is asynchronous */
/*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1;
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}2.2、使用异步Handler
在创建Handler的几种方法一文的第2.1节介绍了两个创建异步Handler的方法,createAsync(Looper looper)方法:
Handler asyncHandler = Handler.createAsync(Looper.getMainLooper());和另一个createAsync(Looper looper,Callback callback)方法,创建带Callback的异步Handler。Handler中的Callback另见Handler中Callback的作用一文。
Handler asyncHandler = Handler.createAsync(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
//TODO
return false;
}
});通过异步Handler发送的Message消息都是异步消息,中间过程详见Handler消息延迟原理一文第3.2节。在enqueueMessage()方法中将会根据Handler自身的mAsynchronous属性,设置Message是否是异步消息:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
//构造异步Handler,mAsynchronous都设置为true。其它构造传入的是false。
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}由此可知,所有通过异步Handler发送的Message,都会用setAsynchronous(true)方法设置成异步消息。
3、如何设置/移除同步屏障 *
MessageQueue中的postSyncBarrier()方法是框架隐藏API,如果想调用它就得使用反射。不难写出反射设置同步屏障的方法:
public static int postSyncBarrier() {
int token = -1;
try {
Method method = MessageQueue.class.getDeclaredMethod("postSyncBarrier");
token = (int) method.invoke(Looper.getMainLooper().getQueue());
} catch (Exception ignore) {
}
return token;
}相应的使用反射,用同步屏障对应的唯一标志token,移除同步屏障:
public static void removeSyncBarrier(int token) {
try {
Method method = MessageQueue.class.getDeclaredMethod("removeSyncBarrier", int.class);
method.invoke(Looper.getMainLooper().getQueue(), token);
} catch (Exception ignore) {
}
}同步屏障的添加与移除必须成对。如果操作失误使得同步屏障不成对,或无法删除同步屏障。会导致主线程只能处理异步消息,同步消息无法执行最终引起应用发生ANR。
开发者应当杜绝反射使用同步屏障,避免发送异步消息。同步屏障和异步消息机制的设计是框架为了保障VSYNC等任务正常执行,以免被应用自身发送的大量消息延后或阻塞。
慎用反射调用框架方法!以免搬起石头砸自己的脚。

