之前介绍IdleHandler机制的时候是因为在看 Matrix 的源码时遇见,IdleHandler 和 Choreographer 不太熟悉,所以就又去看了一下 IdleHandler 的运行机制,然而今天又回过头来看 Choreographer 的时候,发现其中的 FrameDisplayEventReceiver 的 onVsync()
函数 使用到了 msg.setAsynchronous(true)
,发现涉及到一个同步屏障的概念,于是又回过头来看Android消息机制补一补关于同步屏障的知识。
Handler的同步屏障
简单来说,同步屏障就是给消息队列发送了一个屏障信息,消息队列在处理信息的时候发现了屏障信息时就开启了”同步屏障”模式。在该模式下,消息队列中只返回异步消息给 Looper 处理,并且屏蔽同步消息,确保异步消息的优先处理。在处理完异步消息队列后,即使消息队列中还有同步消息也会通过nativePollOnce()
进入线程阻塞状态。直到有新的异步消息进来。除非解除同步屏障
模式,这个时候消息队列才会和正常一样处理同步消息。
同步屏障大概就这样的意思,具体的我们就要根据源码来分析了。所以这里,我们依旧分为三步来分析。首先,通过 同步屏障源码分析同步屏障的工作机制,然后通过源码的分析来得到同步屏障的使用场景,最后列举出系统源码中有哪些地方使用了同步屏障。
源码分析
首先,我们来看下应该如何开启和关闭同步屏障。
同步屏障的开启
/**
// ......
* @hide
*/
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
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) {
// 1
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
// 2
if (when != 0) {
while (p != null && p.when <= 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;
}
return token;
}
}
注释1处,使用了一个 int 值 mNextBarrierToken
作为此次同步屏障的 token, 此 token 用作识别该次的同步屏障会设置为同步屏障消息的参数 arg1
中,之后退出同步屏障也需要使用此token来移除消息队列中的同步屏障消息,从而退出同步屏障。
这里有一点需要注意,我们通常通过 Handler 发送消息的时候都会将 handler 赋值给 message 的 target.
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
// 1
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
例如注释1处,每次向消息队列中加入消息都会将当前发送该信息的 handler 引用赋值给 message。目的是为了在处理消息的时候通过消息的 target 获取到该 handler ,从而处理消息。 而在这里,我们会发现我们只设置了屏障消息的 when
和arg1
参数,没有设置target
参数。这一点,我们可以先留心一下,等到之后还会遇到。
之前我们开启了同步屏障,那么现在我们来看下同步屏障的关闭。
同步屏障的关闭
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;
// 1
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) {
// 2
nativeWake(mPtr);
}
}
}
我们可以看到,在注释1中,通过target == null
和token
来筛选出屏障消息,然后将其移除消息队列,这样就退出了同步屏障。同时,由于退出了同步屏障,所以消息队列中可能会有没有执行的同步消息(普通消息),所以现在在退出同步屏障之后就需要处理这些之前被屏蔽的同步消息了,所以就会根据消息队列中的消息判断是否有消息需要处理来确定needWake
的值,从而在注释2中来调用nativeWake(mPtr)
来唤醒线程,处理消息。
我们可以用图来更直观的表示。
该图是消息队列中的消息,消息分为三种,红色代表屏障消息,蓝色代表同步消息,黄色代表异步消息,消息首部为mMessage
。开启同步屏障的时候传入参数when
决定了屏障消息在消息队列中的位置,一般都是当前时间,也即消息首部。如果此时恰好有消息正在处理,则就会如该图所示。
知道了同步屏障是如何开启关闭的,接下来我们就来看下同步屏障是如何使用的,我们需要从MessageQueue
的next()
看起。
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// 1
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//......
// 将找到的信息返回
}
//......
}
}
我们看下有关同步屏障的代码,注释1处。通过msg.target == null
判断进行同步屏障模式。while 循环中依次遍历消息链表,找出最近的一条异步信息。然后将找到的异步信息返回给 Looper 处理。这样就实现了屏蔽同步消息,优先处理异步消息的功能了。
我们依旧可以看之前那张图,当屏障消息(message2)卡在消息队列中,意味着消息队列进入了同步屏障模式。这个时候 Looper 只处理异步消息(message4, message6),而同步消息(message3,message5)就会一直在消息队列中,直至退出同步屏障模式。
到这里我们就大致了解了同步屏障的实现机制了,但是还有一个小问题我们还没提到,那就是给消息队列中发送异步消息呢。
在创建 Handler 的时候,有一个参数 async
这个bool参数就决定了 handler是同步还是异步 handler。
public Handler(Callback callback, boolean async) {
//......
mAsynchronous = async;
}
当mAsynchronous
为true的时候,handler 发送消息的时候会自动将 Message 设置为异步消息。
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
// 1
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
注释1处,使用msg.setAsynchronous(true)
将消息设置为异步消息,然后插入消息队列中,这就是异步消息的插入过程。
同步屏障的作用
从图或者说源码中我们都可以知道,当同步屏障开启之后,消息队列会自动屏蔽所有的同步消息,然后优先处理异步消息。所以说如果想要有些处理某些消息的时候就可以使用同步屏障机制,并且使用异步 handler 发送异步消息到消息队列中去。而在处理完需要处理的异步消息之后便可以通过移除同步屏障机制来使消息队列恢复正常。
同步屏障在源码中的应用
在View的更新中就使用了同步屏障,目的是为了优先处理绘制界面的三个函数。onLayout()
,onMeasure()
,onDraw()
。例如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();
}
}
Choreographer调用了postCallback
方法, 最终调用了postCallbackDelayedInternal()
方法,也即我刚开始学习Choreographer中遇见的发送异步消息的地方。
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);
}
}
}
至此,我们了解了Handler的同步屏障机制。
小结
发送target == null
的消息到消息队列时开启了同步屏障模式,然后消息队列开始优先处理异步消息。知道退出同步屏障模式(移除屏障消息)之后,消息队列才会处理普通消息(同步消息)。同步屏障机制可以让你优先处理你想要的处理的信息,所以系统源码中使用它来处理View的绘制,因为界面绘制动作应处在第一优先级。但是对我们日常编码而言,该机制可能就作用不大,并且开启同步屏障的代码postSyncBarrier(when)
也是被系统使用@hide
隐藏了。但了解该机制有助于我们理解View的绘制,Choreographer同步信号等与View相关的代码。