今日头条ANR优化实践系列 - Barrier导致主线程假死( 四 )


定位及修复在定位到原因之后,接下来就是找到问题并解决问题,具体什么样的改动会引起这里问题了,通过分析我们知道既然是 Barrier 消息同步的问题,那么我们可以在设置 barrier 和移除 barrier 的过程加入监控,判断哪里设置了 barrier 消息,但是没有同步移除 。通过 Java hook 代理了 MessageQueue 的 postSyncBarrier 和 removeSyncBarrier 接口,进行 Barrier 消息同步监测,遗憾的是线下并没有复现 。
因此只能再次回到代码层面,对相关改动进行分析,最终在一笔需求提交中发现了线索 。
逻辑调整前: 先移除将要强制调度的并设置了异步属性的消息,再强制调度该消息,以保证该消息不受 barrier 消息之前的消息 block,进而提高响应能力 。
if (hasMsg) {    ......    handler.removeCallbacks(message.getCallback()); //先移除    handler.dispatchMessage(cloneMsg); //再强制调度该消息    ......}逻辑调整后: 先强制调度该消息,然后再将该消息从队列中移除 。
    ......        handler.dispatchMessage(newMessage); //先强制调度       handler.removeCallbacks(message.getCallback());  //从队列中移除消息    ......    }但是时序调整后存在一定隐患,即在强制调用 DoFrame 消息期间,业务可能会再次触发 UI 刷新逻辑,产生 barrier 消息并发出 vsync 请求,如果系统及时响应 vsync,并产生 DoFrame 消息,那么调用 removeCallbacks 接口会一次性清除消息队列中所有的 DoFrame 消息,即:移除了消息队列之前的 DoFrame 消息和下次待调度的 DoFrame 消息,但是与下次 DoFrame 消息同步的 barrier 消息并没有被移除 。
那么为什么会移除多个消息呢?这就要从handler.removeCallbacks 的实现说起了 。

今日头条ANR优化实践系列 - Barrier导致主线程假死

文章插图
 
进一步查看
messageQueue.removeMessages 接口实现,发现该接口会遍历消息队列中符合当前 runnable 以及 object 的消息,但是上面传递的 Object 对象是 null,因此就相当于移除了当前 Handler 对象下面所有相同 runnable 对象的消息!
今日头条ANR优化实践系列 - Barrier导致主线程假死

文章插图
 
因为强制刷新和时序调整的问题,导致了消息队列中同时存在 2 个 UI doFrame 消息,并在强制执行之后被同时移除,从而导致一个无人认领的 barrier 消息一直停留在消息队列 !
其它场景:此外,除了上面遇到的场景会导致这类问题之外,还有一种场景也可能会导致这类问题,即:UI 异步刷新,尽管 Android 系统禁止异步刷新,并利用 checkThread 机制对 UI 刷新进行线程检查,但是百密一疏,如果开启硬件加速,在 AndroidO 及之后的版本会间接调用 onDescendantInvalidated 触发 UI 刷新,该逻辑躲过了系统 checkThread 检查,将会造成线程并发隐患 。如下图,如果并发执行则会导致前一个线程的 mTraversalBarrier 被覆盖,从而导致 vsync 消息与 barrier 出现同步问题 。
今日头条ANR优化实践系列 - Barrier导致主线程假死

文章插图
 
查看 Android Q 源码,看到 onDescendantInvalidated 内部加上了 checkThread,但被注释掉了!解释如下:修复摄像头后重新启用或者通过 targetSdk 检查?好吧,或许是忘记这个 TODO 了 。
今日头条ANR优化实践系列 - Barrier导致主线程假死

文章插图
 
总结:至此,我们完成了该类问题的分析和最终定位,综合来看该类问题因 Trace 场景(NativePollOnce)和问题本身的高度隐蔽性,给排查和定位带来了极大挑战,如果单纯依靠系统提供的日志,是很难发现 MessageQueue.next()内部发生了异常 。这里我们通过 Raster 监控工具,还原了问题现场,并提供了重要线索 。现在总结来看,该类问题其实具有很明显的特征,表现在以下几个方面: