2. 常见卡顿类型分析:帧率掉帧、列表滑动不流畅、动画卡顿、触摸延迟、布局重绘过度

做 SystemUI 这么久,每天打交道最多的就是卡顿。说白了,卡顿就是用户觉得「不跟手」「画面跳了一下」「滑动像在泥里」。但同样是卡,背后的原因可大不相同。我个人习惯把常见的卡顿分成五类:帧率跌跤、列表拖泥带水、动画打嗝、触摸慢半拍、布局反复重绘。下面逐个聊聊它们的典型表现和常见病因。

核心提示: 绝大多数卡顿都可以通过 Systrace / Perfetto 定位到具体帧,关键是先识别出是哪一种类型,再顺着线索往下挖。

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 跳过了。
避坑指南: 我经历过一个血泪教训——在 AnimatorUpdateListener 里调了 View.measure(),导致每帧都触发 layout,动画直接从 60fps 跌到 20fps。动画回调里尽量不要做 measure / layout 相关操作,用 setTranslationX / setAlpha 等属性动画就好了。

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 InspectorView 层级和 layout 次数同一帧内多个 View 触发了多次 requestLayout
注意: 布局重绘过度往往不会直接报错,但会导致 CPU 占用异常升高,并且耗电。如果你发现某个场景下 SystemUI 的 GPU 渲染曲线呈锯齿状,但帧率还勉强维持在 55fps 左右,优先检查是否有多余的 layout 触发。

五类卡顿快速对照表

卡顿类型用户感知核心线索常用修复方向
帧率掉帧画面跳、抖Skipped frames > 3主线程去耗时操作
列表滑动不流畅滑动卡、顿、不均匀RecyclerView onBind 耗时ViewHolder 预加载 / 减少绑定计算
动画卡顿动画速度不均匀Animation 回调耗时属性动画代替补间动画;避免回调中 layout
触摸延迟不跟手、反应慢Input dispatch latency > 50ms异步化 Binder 调用;减少主线程阻塞
布局重绘过度耗电、GPU 高负载GPU 模式测量条长、过度绘制红简化层级;减少 measure 触发
我的经验: 在 SystemUI 里,最头疼的往往是多种卡顿类型叠加——比如列表滑动卡顿可能同时伴随着触摸延迟和帧率掉帧。遇到这种情况,建议先用 Systrace 抓 5 秒全局,从上到下看主线程的 Timeline,把每个大块的耗时先揪出来,再对应到上面的类型去深入。
SystemUI 常见卡顿类型知识图谱 卡顿总因 帧率掉帧 列表滑动不流畅 动画卡顿 触摸延迟 布局重绘过度 提示:不同卡顿类型可叠加出现,建议用 Systrace 统一抓取后归类分析

以上就是我平时最常遇到的五类卡顿。记好它们各自的「长相」和常用定位器,后面章节学复现技巧时就能有的放矢。嗯,先消化这些,下次咱们再聊如何「有预谋地制造」这些卡顿。