一、消息机制之同步屏障
消息机制的同步屏障,其实就是阻碍同步消息,只让异步消息通过。而开启同步屏障的方法就是调用下面的方法:
MessageQueue#postSyncBarrier()
源码如下:
@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;
}
}
在这里可以看到,Message对象初始化的时候并没有给target赋值,因此target==null的来源就找得到了。这样就可以插入一条target==null的消息,这个消息就是一个同步屏障。
那么开启消息屏障后,所谓的异步消息又是如何处理的呢?
消息的最终处理其实都是在消息轮询器Looper#loop()中,而loop()循环中会调用MessageQueue#next()从消息队列中进行取消息。
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;
}
...
}
}
}
从上面的MessageQueue.next方法可以看出,当消息队列开启同步屏障的时候(即标识为msg.target==null),消息机制在处理消息的时候,会优先处理异步消息。这样,同步屏障就起到了一种过滤和优先级的作用。
如果上图所示,在消息队列中有同步消息和异步消息(黄色部分)以及一道墙(同步屏障--红色部分)。有了同步屏障的存在,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);
}
}
}
二、同步屏障的应用场景
同步屏障一般在日常开发中比较少用,而在系统源码中就有使用。Android系统中的UI更新相关的消息即为异步消息,需要优先处理。
16ms左右刷新UI,而是60hz的屏幕,即1s刷新60次。
在Android中什么是异步消息?即给:
Message.setAsynchronous(true);
比如,在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()
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);
}
}
}
可以看到,这里就开启了同步屏障,并且发送了异步消息。由于UI相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。
最后,当然要移除同步屏障的时候,调用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()中就会移除同步屏障,因为此时消息已经发送并且处理了。