版权声明:转载必须注明本文转自严振杰的博客:http://blog.yanzhenjie.com
想要了解清楚同步障碍机制,必须先完全理解Android 消息机制和应用,不了解的同学可以先去搜索相关资料熟悉一下再继续看下面的内容。
我们知道消息机制中有一个重要的类是MessageQueue
,望文生义就是消息队列的意思,在一般情况下,MessageQueue
对于当前线程是同步的,那么什么是当前线程呢?就是实例化MessageQueue
的线程,在消息机制这个完整的机制中,MessageQueue
是在Looper
的构造方法中被实例化。也就是说,MessageQueue
正常情况是同步处理消息的,明白这一点就可以让同步障碍入场了。
同步障碍,看字面意思也能猜出个八九分,就是阻碍队列中同步消息的障碍,那么它是如何运行的呢?此时我们得先引入异步消息。
在消息机制中,还有一个异步消息,在正常时候,我们发送的Message
全都是同步消息,当然我们也可以发送异步消息,发送异步消息有两种方式,下面分别来看一下。
使用Handler
包含async
参数的构造方法实例化Handler
,例如下面这个:
public Handler(boolean async) {
this(null, async);
}
只要async
参数为true
,所有的消息都将是异步消息,例如我们一般情况下会这样发送消息:
Handler mHandler = new Handler(true);
...
Message msg = mHandler.obtainMessage(...);
mHandler.sendMessageAtTime(msg, ...);
Message
为异步消息Message msg = mHandler.obtainMessage(...);
msg.setAsynchronous(true);
mHandler.sendToTarget();
其实第一种方式也是在我们发送Message
时,Handler
内部帮我们调用了Message#setAsynchronous(boolean)
,且参数传了true
,因此这两使用方式了解即可。
我们知道,上面的Message#target
都是非空的,而在MessageQueue#next()
方法中,target
非空的Message
都会被正常处理(下面会有相关代码),因此在这个时候同步消息和异步消息并没有什么不同。如果同学们思路没有断的话,应该能想到,此时同步障碍就需要登场了。
现在我们要贴一点framework/base
下的代码,都以oreo-release
分支为准,因为不同版本这些代码所在的位置略有不同。
如果想让异步消息起作用,就得开启同步障碍,同步障碍会阻碍同步消息,只允许通过异步消息,如果队列中没有异步消息,此时的loop()
方法将被Linux epoll
机制所阻塞。
开启同步障碍也很简单,调用MessageQueue#postSyncBarrier()
方法即可,因为MessageQeueu
绑定在Looper
上,而Looper
依附在Handler
上,所以正常情况下,源码中是这样开启同步障碍的:
mHandler.getLooper().getQueue().postSyncBarrier();
它所调用的代码也很简单:
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
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;
}
可以看出,在实例化Message
对象的时候并没有设置它的target
成员变量的值,然后随即就根据执行时间把它放到链表的某个位置了,实际上就是链表的开始位置。也就是说,当在消息队列中放入一个target
为空的Message
的时候,当前Handler
的这一套消息机制就开启了同步障碍。
当开启同步障碍后,它是如何生效的呢?我们知道,Looper#loop()
方法最终还是调用了MessageQueue#next()
方法来获取队列中的消息,现在我们来看看该方法的代码:
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 如果当前开启了同步障碍
if (msg != null && msg.target == null) {
// 处理同步障碍,获取队列中的下一个异步消息
do {
prevMsg = msg;
msg = msg.next;
// 如果这个消息是同步的,那么继续向下找异步的
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// 如果当前消息的执行时间没到,让它沉睡到下个消息的执行时间
nextPollTimeoutMillis = msg.when - now;
} else {
...
return msg; // 执行时间到了,执行它
}
} else {
// 当前没有任何消息需执行,一直沉睡
nextPollTimeoutMillis = -1;
}
}
}
我摘抄出了主要的代码,为了方便同学们阅读理解,上述代码是简化过的。
可以看出来,当开启了同步障碍时,Looper
在获取下一个要执行的消息时,会在链表中寻找第一个要执行的异步消息,如果没有找到异步消息,就让当前线程沉睡。
nativeWake()
方法和nativePollOnce()
方法采用了Linux epoll
机制,其中nativePollOnce()
的第二个值,当它是-1
时会一直沉睡,直到被主动唤醒为止,当它是0
时不会沉睡,当它是大于0
的值时会沉睡传入的值那么多的毫秒时间。epoll
机制实质上是让 CPU 沉睡,来保障当前线程一直在运行而不中断或者卡死,这也是Looper#loop()
死循环为什么不会导致住线程 ANR 的根本原因。
那么同步障碍有什么用呢?在我们日常的应用层开发中极少用到它,读了framework
的代码后我们会发现,在ViewRootImpl.java
中有使用它:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 开启当前 Handler 的同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 发送一条异步消息
mChoreographer.postCallback(..., mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
cheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
实际上,这里的Handler
使用的是主线程的Looper
,因此这里会阻断主线程Looper
的其他同步消息,在ViewRootImpl
和Choreographer
中多次使用到了异步消息,以完成View
的整个绘制流程。
没错,也许有同学已经被启发了,当我们点击页面的某个控件时,希望瞬间得到它的回应,而不是卡在那里,最起码有个圈圈在转也行。当我们点击某个按钮,此时开启了一个Activity
,如果队列中此时有很多消息在排队等候呢?那么这个Activity
的测量、布局和绘制就得一直等到所有消息被处理完成才能执行,此时我们会看到页面一直黑着或者一直白着,反正不是我们想要的效果,因此如果这个消息队列有一个优先级的特点,那么不就可以解决这个问题了吗?
综上,所以在消息机制中也很巧妙的融入了队列优先级的特点,这个同步障碍机制,实质上是一个对消息队列的优先级实现。关于Java/Android 中的优先级任务队列的实践,大家可以多搜索相关资料来学习,这个在日常开发中很有用。
就到这里了,告辞!
版权声明:转载必须注明本文转自严振杰的博客:http://blog.yanzhenjie.com