Android Handler源码解析-同步屏障
前言
在平时正常开发过程中我们很少会遇到使用同步屏障,甚至可能都不知道有这么个东西,但是要深入了解Handler机制,同步屏障机制却是必不可少的部分。那么什么是同步屏障?
什么是同步屏障
上一篇文章说过,MessageQueue不是一个队列,但是MessageQueue符合队列的出队定义,那么出队的时候由于每条Message的执行时间不同,就有可能造成队列拥挤。本来是某时刻需要执行的一条消息可能就没法准时执行,若这条消息是屏幕触摸,屏幕刷新之类的消息就会造成操作延迟,显然这是系统不愿意看到的,于是同步屏障机制就出现了。
Handler消息分为三种
- 同步消息
- 异步消息
- 屏障消息
我们之前介绍的消息都属于同步消息,而异步消息和屏障消息实际上是组合使用的,使用异步消息的同时也会用到屏障消息。
所谓同步屏障,就是在MessageQueue里设置一道屏障,阻碍同步消息通过,允许异步消息通过。
异步消息
先来看看怎么设置异步消息
//#Handler
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;
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
//设置msg为异步消息
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
设置异步消息非常简单,Message#setAsynchronous设置为true就是一条异步消息,或者构造Handler时传入async为true,那么通过Handler发送的Message都是异步消息。
屏障消息
//#MessageQueue
//设置屏障消息
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
//设置屏障消息,插入的位置为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) {
final int token = mNextBarrierToken++;//屏障消息的身份令牌,代表一条屏障消息,移除屏障消息时用到
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
//下面的操作和插入普通消息差不多。判断是否插入头结点,若不是则遍历链表,根据when插入到链表对应的位置
Message prev = null;
Message p = mMessages;
if (when != 0) {
//根据when遍历链表找到对应位置
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // 插入对应位置
msg.next = p;
prev.next = msg;
} else {//插入头部
msg.next = p;
mMessages = msg;
}
return token;
}
}
MessageQueue提供了一个postSyncBarrier方法设置屏障消息,这个屏障消息的位置在MessageQueue的队首,会拦截MessageQueue里的所有同步消息。若只想拦截在MessageQueue某个节点之后的同步消息,可以通过反射去调用postSyncBarrier有when参数的那个方法。
从代码可以看到,屏障消息是没有设置target的,那么屏障消息实际就是一个没有target的普通消息
同步屏障原理
同步屏障说起来很简单,那么开启同步屏障后到底是怎么过滤屏蔽掉同步消息的呢?
//#MessageQueue
Message next() {
int nextPollTimeoutMillis = 0;
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//#1 这里判断msg的target是否为null,如果是null,那msg就是屏障消息,然后会去取msg后面的第一条异步消息
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());//判断msg的next是否是异步消息,如果不是则继续循环拿next,直到拿到异步消息或者没有next
}
//#2
if (msg != null) {
if (now < msg.when) {//如果当前时间小于msg的时间点,说明msg执行时间还未到
//设置阻塞时间
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {//执行时间已到,从链表中取出msg这个节点并返回
mBlocked = false;
if (prevMsg != null) {//prevMsg不为空,说明msg是异步消息
prevMsg.next = msg.next;//将msg的next设为msg上一个消息的next,等于是从消息链表中取出msg
} else {//同步消息,msg就是mMessages
mMessages = msg.next;//将msg的next设为新的mMessages,等于是从消息链表中取出头结点msg
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {//MessageQueue没有消息,进入阻塞
nextPollTimeoutMillis = -1;
}
nextPollTimeoutMillis = 0;
}
}
next()方法在上一篇博客里已经介绍过了,但当时还有#1处的代码没有介绍,这处代码就是关于屏障消息的。
可以看到,next的for循环每次都会将mMessages设为msg,所以#1处代码其实就是判断消息链表的头结点是否是屏障消息,如果不是屏障消息,就走正常流程#2。如果是屏障消息,就通过循环去取链表里面的第一条异步消息,之后也是走#2。
#2处代码,同步消息和异步消息不同之处在于,同步消息是断开头结点取出msg,而异步消息是从链表中间断开取出msg。
移除同步屏障
看完上面的文章,想必大家肯定能想到,移除同步屏障只要把屏障消息移除了就行。
的确如此,我们看代码。
public void removeSyncBarrier(int token) {//token是设置屏障消息时生成的
synchronized (this) {
Message prev = null;
Message p = mMessages;
while (p != null && (p.target != null || p.arg1 != token)) {//如果不是屏障消息或者token不一致,就继续找next,直到找到要移除的屏障消息p
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;
//移除节点p
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
//回收这个消息
p.recycleUnchecked();
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
上面的代码简单来说,就是根据token找到对应的屏障消息,然后从链表删除这个屏障消息。