2. 常见卡顿类型分析:帧率掉帧、列表滑动不流畅、动画卡顿、触摸延迟、布局重绘过度
做 SystemUI 这么久,每天打交道最多的就是卡顿。说白了,卡顿就是用户觉得「不跟手」「画面跳了一下」「滑动像在泥里」。但同样是卡,背后的原因可大不相同。我个人习惯把常见的卡顿分成五类:帧率跌跤、列表拖泥带水、动画打嗝、触摸慢半拍、布局反复重绘。下面逐个聊聊它们的典型表现和常见病因。
2.1 帧率掉帧(Frame Drops)
帧率掉帧是最直白的卡顿——一秒钟本该画 60 帧,结果中间缺了几帧,人眼就能感觉到画面一跳一跳的。我在项目中遇到过好几次,用户反馈下拉状态栏时中间会有「丢帧感」,其实就是某几帧的绘制时间超过了 16.6ms。
- 典型表现: 快速滑动、动画过程中画面出现肉眼可见的停顿,且停顿时间短(几十到几百毫秒),频率不固定。
- 排查重点: Choreographer 的 doFrame 回调中,看 mSkippedFrames 的值。如果大于 3,说明 Choreographer 没赶上 Vsync。
// 在 logcat 中过滤:
adb logcat -s Choreographer:V
// 你会看到类似:
Choreographer: Skipped 3 frames! The application may be doing too much work on its main thread.
为什么会跳过?绝大多数情况是主线程被某个耗时操作阻塞了。比如我排查过一个 case,NotificationPanelView 的 onDraw 里居然做了 Bitmap 的 decode……嗯,这种操作就该拉到后台线程去。
2.2 列表滑动不流畅(Janky List Scrolling)
SystemUI 里有很多列表:快速设置面板、通知列表、最近应用切换等。滑动不流畅常见三种表现:拖动时突然「卡住」一下然后弹走、滑动速度不均匀、松手后惯性滚动一顿一顿的。
| 表现 | 可能原因 | 我常用的定位手段 |
|---|---|---|
| 滑动中抖动 | Item 高度变化 / 异步加载图片 | 检查 RecyclerView 的 onBindViewHolder 耗时 |
| 滚动起始阶段卡顿 | 布局 inflate 或者 View 创建延迟 | 观察 RecyclerView 的 prefetch 是否失效 |
| 惯性滑动不均匀 | OverScroller 的 fling 被频繁打断 | 查 TouchSlop 或嵌套滑动冲突 |
我记得有一次 QuickSettings 的 Tile 列表在快速滑动时总是掉帧,查到最后发现每个 Tile 的 onBind 里都调了一次 Resources.getDrawable(),而且没有做缓存。说白了,每划过一个 Tile 都要重新加载图标,不卡才怪。改成预加载后,帧率直接拉满。
2.3 动画卡顿(Animation Stutter)
动画卡顿和单纯掉帧还不一样——它更像是动画「跑一半停了,然后又加速追回来」。典型场景:展开通知面板时,背景模糊动画一卡一卡;锁屏到主界面过渡动画中间有跳跃。
- 典型表现: 动画的起止速度不一致,中间出现明显的速率突变,或者动画被截断后再恢复。
- 核心原因: ValueAnimator 或 ObjectAnimator 的回调里做了耗时的 UI 更新,或者动画帧被 Choreographer 跳过了。
2.4 触摸延迟(Touch Latency)
触摸延迟更隐蔽——用户感觉手指动了一下,但画面隔了一拍才跟上。它不一定掉帧,但就是「不跟手」。SystemUI 里很多触摸事件要经过 InputDispatcher → WindowInputEventReceiver → View 体系,中间任何一个环节被阻塞都会带来延迟。
// 检查触摸响应延迟的常用方法:
adb shell dumpsys input – 看 dispatch latency
// 如果 dispatch 耗时 > 100ms,大概率是输入事件分发的处理线程被阻塞了
- 典型表现: 点按后响应慢(不是掉帧,就是反应慢);滑动开始时感觉手指和画面之间有「黏滞感」。
- 常见元凶: 主线程被 Binder 调用卡住,或者某个 View 的 onTouchEvent 里做了同步网络请求。
我曾经排查过一个 case:在 StatusBar 的 onInterceptTouchEvent 中,为了判断是否拦截事件,调用了 WindowManager 的一个跨进程调用,结果有时要等 50ms 才返回。触摸延迟就上来了。改成异步判断后,响应恢复正常。
2.5 布局重绘过度(Excessive Layout / Relayout)
布局重绘过度是一种「隐形杀手」——画面没崩,帧率也够,但 GPU 渲染的警告条总是高亮。说白了,就是同一帧里 layout 和 measure 被调了太多次。
- 典型表现: Profile GPU Rendering 的条形图里,绿色(measure/layout)和蓝色(draw)占比重特别大;或者开发者选项里的「过度绘制」出现大面积红色。
- 我在项目中遇到过的: NotificationContentView 在 inflate 时,因为使用了 ConstraintLayout 的百分比约束,再加上动态文字变化,导致每刷新一次就触发整个视图树的重新 layout。后来改成固定比例 + 用 LinearLayout 权重替代,重绘次数降了一半。
| 检测工具 | 关注指标 | 危险信号 |
|---|---|---|
| GPU 呈现模式分析 | 每帧的 measure/layout 耗时 | 耗时 > 5ms 需警惕 |
| 过度绘制调试 | 屏幕颜色(蓝→绿→淡红→深红) | 淡红区域持续存在说明有冗余重绘 |
| Layout Inspector | View 层级和 layout 次数 | 同一帧内多个 View 触发了多次 requestLayout |
五类卡顿快速对照表
| 卡顿类型 | 用户感知 | 核心线索 | 常用修复方向 |
|---|---|---|---|
| 帧率掉帧 | 画面跳、抖 | Skipped frames > 3 | 主线程去耗时操作 |
| 列表滑动不流畅 | 滑动卡、顿、不均匀 | RecyclerView onBind 耗时 | ViewHolder 预加载 / 减少绑定计算 |
| 动画卡顿 | 动画速度不均匀 | Animation 回调耗时 | 属性动画代替补间动画;避免回调中 layout |
| 触摸延迟 | 不跟手、反应慢 | Input dispatch latency > 50ms | 异步化 Binder 调用;减少主线程阻塞 |
| 布局重绘过度 | 耗电、GPU 高负载 | GPU 模式测量条长、过度绘制红 | 简化层级;减少 measure 触发 |
以上就是我平时最常遇到的五类卡顿。记好它们各自的「长相」和常用定位器,后面章节学复现技巧时就能有的放矢。嗯,先消化这些,下次咱们再聊如何「有预谋地制造」这些卡顿。