最近遇到一个bug,app在使用中偶尔会出现界面不刷新,按钮也不响应,但是并没有ANR,process也不会被系统杀死,其他应用程序运行正常,该状态一直会被保持直到手动杀死app或者重启系统。
搜遍全网也没发现相关信息,后来经过反复在framework里加log,反复测试,终于找到了原因。
原因是在某个极端情况下,系统在app的UI 线程消息队列中遗留了一个Barrier Message(屏障消息)没有清除,导致后面所有同步消息和runnable都没有办法得到执行。
原理:
BarrierMessage(屏障消息)用来在UI需要刷新时暂时让其他消息不能执行,它在需要UI刷新时被添加到UI消息队里的头部,代码在ViewRootImpl的scheduleTraversals里:
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
scheduleTraversals在UI操作时会被调用到,比如setText,setSelected,以及其他主动UI操作,其中的mHandler.getLooper().getQueue().postSyncBarrier()会在app的UI线程消息队列上添加一个屏障消息BarrierMessage,添加后会返回该消息id并保存在mTraversalBarrier 里,在UI刷新完成后用该id来删除该屏障消息,参见doTraversal
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
当UI消息队里有屏障消息BarrierMessage时,消息轮询会跳过所有的同步消息(缺省都是同步消息),参见代码MessageQueue里的next函数:
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());
}
问题:
根据上面的原理可以知道如果消息队列里一直有屏障消息则其他消息都不能得到执行,在通常情况下当屏障消息被添加后UI刷新会马上执行,然后马上该屏障消息会被remove,所以都没有问题。
但是在代码中我们可以注意到一个问题,scheduleTraversals没有同步锁! 虽然在大多数地方调用改方法之前有checkThread来检查线程,但是还有其他几个地方没有,比如invalidate方法,所以在极端情况下如果app实现不当,通过后台线程调用了UI刷新操作,同时UI线程也在执行刷新,则会造成scheduleTraversals被同时调用,但是由于只有一个mTraversalBarrier 来保存消息id,所以mTraversalBarrier 只会保留最后一个屏障消息的id,当清除屏障消息时只会清除mTraversalBarrier 对应的消息,另外一个消息则不能被清除掉,会永远留在UI消息队列中,如下图:
mTraversalScheduled = false
线程1 线程2
mTraversalScheduled是false?->yes mTraversalScheduled是false?->yes
mTraversalBarrier = 10
mTraversalBarrier = 11
屏障消息11 | 屏障消息10 | 常规消息 | 常规消息 | 常规消息 |
根据mTraversalBarrier 移除屏障消息11后消息10还留在消息队列中:
屏障消息10 | 常规消息 | 常规消息 | 常规消息 | 常规消息 |
根据上面的原理就会造成后续所有放到UI消息队列中的操作无法继续进行,包括按钮的onClick也不能得到执行,因为它会通过post runnable到消息队列来执行的,看到的现象就是按钮点击后没反应,UI也不刷新,但是只要不是post的消息队列中的操作还可以继续执行,比如在touch事件中做什么事,后台线程也在继续运行。
解决:
知道了问题的原因后就好解决了:
1. 在实现app的时候确保UI的操作不能从后台线程执行,虽然通常情况下应用会crash,但是有些UI方法如setSelected没有线程检查,应用会继续运行,在没有发生上述的情况下我们很难注意到这个问题,只有靠静态代码检查。
2. 修改framework代码为scheduleTraversals添加同步锁,需要大量测试来验证不会造成regression
3. 修改MessageQueue,在清除屏障消息BarrierMessage时清除所有可能存在的屏障消息,没有验证过,同样需要大量测试来验证
其它方案。。。
如果你遇到过这个问题,请留下你的解决方案,谢谢阅读~~~