问题
Handler同步屏障是否会导致ANR?
结论
同步屏障的使用有可能会导致ANR
分析
- 什么是同步屏障
同步屏障,简单理解就是阻塞同步消息,也就是我们平常使用handler发送的消息 - 什么是垂直同步信号
参考屏幕刷新机制
参考Handler原理
这里简单理解就是Choreographer每16ms回调doFrame,内部原理参考屏幕刷新机制
源码分析 ViewRootImpl#invalidate#scheduleTraversals
ViewRootImpl#invalidate#scheduleTraversals
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//通过Handler发送一个同步屏障到消息队列中
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//通过Choreographer发送callBack,等待垂直同步信号vsync回调doFrame
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
//doFrame回调后,调用doTraversal方法
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
从上述可以源码可以分析得知,我们在调用View刷新的时候,会调用到View#invalid,从而触发到ViewRootImpl#scheduleTraversals,继而会添加同步屏障到消息队列,来阻塞MessageQueue的普通消息,等待mTraversalRunnable#Choreographer#doFrame()回调后,移除掉当前的同步屏障,让其他普通消息,能够正常消费。
那么MessageQueue是如何判断是否需要阻塞的?MessageQueue#next
Message next() {
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) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
}
}
通过源码可知,MessageQueue在轮训消息的时候,执行next()方法,会不断去获取msg对象,如果msg.target ==null,那么它就是屏障,需要循环遍历,一直往后找到第一个异步的消息。同样的,当我们移除掉同步屏障之后,也就不会执行当前判断体内,如下源码分析
//移除同步屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
MessageQueue####
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) {
//从消息链表中移除
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
p.recycleUnchecked();
}
Message##### 回收这个Message到对象池中。
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
重点看这里,移除屏障消息。
if (prev != null) {
prev.next = p.next;
needWake = false;
} 。
到此基础的同步屏障理论基本串联起来,我们正常在使用View更新的时候,都会走到同步屏障里面来,为了保障消息的优先级。
- 到此也可以分析一个问题。如果同步屏障的消息没有及时处理掉,是因为Choreographer#doFrame没有及时回调,导致后续的普通消息被阻塞,没有在5S内消费掉,从而产生ANR。
需要注意的地方
- 不要在子线程做view#invalid的操作,根据上述源码,我们可以知道,控制同步屏障的添加跟移除是通过mTraversalScheduled这个变量来控制的,会出现多线程的异常,严重的可能导致,多线程多次调用iew#invalid,导致同步屏蔽被多次添加到队列中,但是确没有完全移除。这就是谷歌推荐我们如果需要在子线程更新UI。需要使用postInvalidate。
网友评论