4 输入事件ANR案例:主线程触屏事件阻塞,定位到耗时布局绘制
4.1 现象识别
用户在某个页面频繁滑动或点击时,界面突然无法响应触摸事件。几秒后弹出系统ANR对话框,日志关键字如下:
ANR in com.example.app (com.example.app/.MainActivity)
PID: 12345
Reason: Input event dispatching timed out (Waiting to send key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago. Wait queue length: 3)
同时traces.txt中主线程堆栈明确卡在View.draw()或ViewGroup.drawChild()链路上,伴随大量Canvas.drawBitmap、Path.op或Layout.draw调用。
4.2 数据采集
使用以下命令采集ANR现场信息:
# 获取主线程堆栈
adb shell cat /data/anr/traces.txt | grep -A 30 "main"
# 检查绘制性能
adb shell dumpsys gfxinfo com.example.app
# 打开GPU过度绘制调试(开发者选项)
adb shell setprop debug.hwui.overdraw true
在traces.txt中定位到主线程卡在如下调用链(示意):
"main" prio=5 tid=1 Blocked
at android.view.ViewGroup.drawChild(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4280)
at android.widget.FrameLayout.draw(FrameLayout.java:600)
at com.example.app.CustomDrawView.onDraw(CustomDrawView.java:45)
at android.view.View.draw(View.java:22000)
at android.view.ViewGroup.drawChild(ViewGroup.java:4500)
...
4.3 原因定位
通过故障树逐层排查:
结合堆栈和dumpsys gfxinfo数据,进一步排查表如下:
| 判断指标 | 典型值 | 临界阈值 | 判定规则 |
|---|---|---|---|
| Draw耗时 | > 16ms/帧 | 单帧绘制 > 100ms | 主线程长时间卡在draw → 布局/自定义View过重 |
| 过度绘制层数 | 2-3层 | > 5层(红色区域) | 冗余背景/重叠View,导致GPU开销暴增 |
| OnDraw中对象分配 | 0个 | 每帧新对象 > 10 | 导致GC频繁触发,阻塞UI线程 |
| 布局层级深度 | < 10层 | > 15层 | 嵌套过深,measure/layout耗时指数增长 |
🔥 快速判断技巧: 若
traces.txt中主线程栈顶为onDraw或dispatchDraw,且下方无锁等待,则95%是布局绘制耗时导致。可用Profile GPU Rendering工具验证。
4.4 修复动作
定位到CustomDrawView.onDraw中每帧创建了Paint和Path对象,并进行了复杂贝塞尔曲线运算。优化策略:
- 将对象创建移到构造方法或成员变量,避免每帧重复分配。
- 简化视图层级:使用
ConstraintLayout减少嵌套,移除无用的背景图层。 - 启用硬件加速:在清单文件中为Activity添加
android:hardwareAccelerated="true"。 - 利用
ViewStub或merge标签延迟加载非首屏布局。
修复前后关键代码对比:
// 修复前(onDraw内每帧创建对象)
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setAntiAlias(true);
Path path = new Path();
path.moveTo(0, 0);
path.cubicTo(100, 200, 300, 400, 500, 0);
canvas.drawPath(path, paint);
}
// 修复后(对象复用)
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path path = new Path();
@Override
protected void onDraw(Canvas canvas) {
path.reset();
path.moveTo(0, 0);
path.cubicTo(100, 200, 300, 400, 500, 0);
canvas.drawPath(path, paint);
}
4.5 复测
修复后再次执行相同交互场景:
- 连续快速滑动列表 30 秒 → 无ANR。
- 抓取
dumpsys gfxinfo,查看Draw耗时降至 3-5ms/帧。 - 使用
adb shell dumpsys activity processes确认主线程不再有“waiting for input”状态。
# 复测命令
adb shell dumpsys gfxinfo com.example.app | grep "Draw"
Draw: 3.2ms (avg), 2.1ms (min), 4.8ms (max)
4.6 预防措施
⚠️ 预防清单: 以下规则应在开发阶段和CodeReview中强制执行:
- 布局深度不超过10层,使用
Layout Inspector定期检查。 - 自定义View的onDraw禁止分配对象(可使用Lint规则:
Avoid object allocations during draw/layout)。 - 启用界面卡顿检测:集成
BlockCanary或StrictMode,主线程超过200ms即报警。 - 定期使用Profile GPU Rendering进行回归测试,确保每帧绘制时间 < 16ms。
- 复杂度高的绘制操作(如剪裁、贝塞尔、Path特效)使用
SurfaceView或RenderNode异步化。
4.7 本章小结
本案例演示了输入事件ANR的完整排查过程:从“触屏无响应”现象切入,通过traces.txt锁定主线程卡在布局绘制,运用故障树和排查表量化归因,最终通过对象复用、视图简化和硬件加速修复。该流程可复用于90%以上因布局绘制导致的触屏ANR场景。