在不是你挑的硬件上追掉帧
“每秒 60 帧”是关于平均值的说法,“卡顿”是关于最差那几帧的说法。这是两种不同的测量,而用户能感觉到的只有后者。
帧预算是固定的
60 Hz 屏上,每帧 16.7 毫秒;120 Hz ProMotion 上是 8.3 毫秒。这个预算不会因为芯片更快就变大 —— 芯片更快只意味着能塞进去的活更多。
所以在最新款上从不出现的卡顿,在三年前的机型上会稳定出现。旧设备没有毛病,只是你的活装不进它的预算。
看下陷
采一段会卡的交互,按顺序做三件事:
- 找曲线掉下去的位置。 不是看平均,是看下陷。
- 记下它们发生的时机。 启动?第一次滑动?每次出图片?转场时?
- 读那一刻的 CPU、GPU 和内存。 诊断就在这一步。
| 下陷时刻 | 你处在 |
|---|---|
| CPU 冲高、GPU 平 | 主线程 CPU 受限 |
| GPU 冲高、CPU 平 | 渲染受限 |
| 内存跳升 | 帧中途做了一次大分配 |
| 网络在动 | 在不该等 I/O 的地方等 I/O |
| 什么都没动 | 阻塞在进程之外,或者在降频 |
四条曲线加一个时间戳,大部分情况一分钟内就能定性。
五个常见成因
解码原图
一张 4000 像素的照片放进 200 像素的 cell,仍然按 4000 像素解码。它吃 CPU、要一次大分配,如果发生在滑动过程中,就会吃掉帧。显示前先降采样,并且放到非主线程去做。
在列表密集的 App 里,这是滑动卡顿最常见的单一成因,也是最好修的。
随内容规模增长的布局工作
约束求解不是随视图数量线性增长的。每个 cell 都重建的深层级,表现出来是 CPU 随滑动速度上升 —— 用户滑得越快越卡,恰好和大家的直觉相反。
阴影与模糊
在桌面 GPU 上很便宜,在手机上很贵。运动中的视图带阴影而没有明确的 shadowPath,系统每帧都要重新计算形状。模糊更糟:那是一次离屏渲染,叠几层就足以让你在没写任何昂贵代码的情况下变成渲染受限。
滑动回调里的同步操作
拖动过程中碰磁盘、碰钥匙串、碰网络,一定看得出来。UserDefaults 的读取看起来是免费的,直到它出现在一个每秒触发六十次的回调里。
做了马上就要扔掉的活
预取六屏之外的内容、给永远不会被看到的 cell 排版、为了三个字段解析整个响应。单独看都没错,但它们抢的是同一个 16.7 毫秒。
不会体现在 FPS 上的卡顿
有些体感最差的问题,根本不会让帧率曲线动一下:
- 点下去 300 毫秒才有反应。那几帧都很正常,只是里面什么都没发生。
- 动画起得晚。起来之后很流畅,但比手指抬起晚了一拍。
- 转场掉了一帧。统计上看不见,体感上非常明显。
这几类要看交互前后的 CPU 曲线,并对照日志读。拖动过程中日志输出出现一段空白,往往是“确实有东西阻塞了”最清晰的证据。
让结论站得住的测法
- 用你支持的最旧机型。 那里流畅,别处就都流畅。
- 先让设备凉下来。 降频状态下的采集,量的是温度。
- 重复同一段交互。 同一个列表、同样距离、同样速度。
- 在数据线上采。 无线采样自带抖动,你会把它当成 bug。
- 对比两次采集,而不是看一次。 改动前后各一次。单次采集只是一个数字,两次才是结果。
到哪儿为止
在一台三代前的设备上追最后那两帧,通常不是价值所在。目标不是一条平直的线,而是“用户高频操作的那些交互里,没有一次他能察觉的下陷”。把那些修掉,在你支持的最慢硬件上验证一遍,然后收工。
