問題
Handler同步屏障是否會導致ANR?
結論
同步屏障的使用有可能會導致ANR
分析
- 什么是同步屏障
同步屏障,簡單理解就是阻塞同步消息,也就是我們平常使用handler發(fā)送的消息 - 什么是垂直同步信號
參考屏幕刷新機制
參考Handler原理
這里簡單理解就是Choreographer每16ms回調(diào)doFrame,內(nèi)部原理參考屏幕刷新機制
源碼分析 ViewRootImpl#invalidate#scheduleTraversals
ViewRootImpl#invalidate#scheduleTraversals
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//通過Handler發(fā)送一個同步屏障到消息隊列中
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//通過Choreographer發(fā)送callBack,等待垂直同步信號vsync回調(diào)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回調(diào)后,調(diào)用doTraversal方法
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
從上述可以源碼可以分析得知,我們在調(diào)用View刷新的時候,會調(diào)用到View#invalid,從而觸發(fā)到ViewRootImpl#scheduleTraversals,繼而會添加同步屏障到消息隊列,來阻塞MessageQueue的普通消息,等待mTraversalRunnable#Choreographer#doFrame()回調(diào)后,移除掉當前的同步屏障,讓其他普通消息,能夠正常消費。
那么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在輪訓消息的時候,執(zhí)行next()方法,會不斷去獲取msg對象,如果msg.target ==null,那么它就是屏障,需要循環(huán)遍歷,一直往后找到第一個異步的消息。同樣的,當我們移除掉同步屏障之后,也就不會執(zhí)行當前判斷體內(nèi),如下源碼分析
//移除同步屏障消息
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;
} 。
到此基礎的同步屏障理論基本串聯(lián)起來,我們正常在使用View更新的時候,都會走到同步屏障里面來,為了保障消息的優(yōu)先級。
- 到此也可以分析一個問題。如果同步屏障的消息沒有及時處理掉,是因為Choreographer#doFrame沒有及時回調(diào),導致后續(xù)的普通消息被阻塞,沒有在5S內(nèi)消費掉,從而產(chǎn)生ANR。
需要注意的地方
- 不要在子線程做view#invalid的操作,根據(jù)上述源碼,我們可以知道,控制同步屏障的添加跟移除是通過mTraversalScheduled這個變量來控制的,會出現(xiàn)多線程的異常,嚴重的可能導致,多線程多次調(diào)用iew#invalid,導致同步屏蔽被多次添加到隊列中,但是確沒有完全移除。這就是谷歌推薦我們?nèi)绻枰谧泳€程更新UI。需要使用postInvalidate。