从卡顿到流畅:iOS 列表性能排查清单
列表卡顿往往不是某一行代码造成的。用一次可重复的测量,把主线程工作、图片解码、布局和数据更新逐层拆开。
列表滚动不流畅时,直接开始改 cell 往往效率不高。掉帧可能来自图片解码、布局计算、主线程 I/O、批量状态更新,甚至是屏幕外完全无关的定时任务。
一套稳定的排查顺序,可以减少“改了很多,但不知道哪一处有效”的情况。
先定义可重复的场景
性能问题必须能重复测量。开始前记录:
- 设备型号与系统版本;
- Debug 还是 Release 构建;
- 数据量、图片大小和缓存状态;
- 操作路径,例如冷启动后首次快速滑动;
- 关注的指标,例如掉帧、主线程阻塞或内存峰值。
真机上的 Release 或接近 Release 的配置更有参考价值。调试器、额外断言和日志都可能显著改变结果。
检查主线程上的意外工作
先用 Instruments 的 Time Profiler 和 Hangs 模板观察卡顿时间段。重点不是找“耗时最长的函数”,而是找滚动期间本不该出现在主线程的工作。
常见问题包括:
- 同步读取文件或数据库;
- 大图首次解码;
- 日期、富文本或正则表达式重复构建;
- 日志把大对象转换为字符串;
- cell 配置阶段执行网络缓存查询;
- 每次滚动都重新计算不变的数据。
给关键阶段添加 signpost,可以让代码事件和帧时间对齐:
import os.signpost
private let log = OSLog(subsystem: "ink.teem.notes", category: "List")
let state = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "ConfigureCell", signpostID: state)
configure(cell, with: model)
os_signpost(.end, log: log, name: "ConfigureCell", signpostID: state)
把图片成本拆开
图片列表通常同时包含三类成本:下载、解码和缩放。网络完成不代表图片已经能低成本显示;压缩数据首次绘制时仍可能在主线程触发解码。
建议确认:
- 请求尺寸是否接近屏幕实际显示尺寸;
- 解码和降采样是否在后台完成;
- 缓存键是否包含尺寸与显示模式;
- cell 复用后,旧请求是否会覆盖新内容;
- 快速滚动时是否取消了不可见资源请求。
override func prepareForReuse() {
super.prepareForReuse()
representedID = nil
imageTask?.cancel()
imageTask = nil
thumbnailView.image = placeholder
}
仅仅在回调里比较 indexPath 不够可靠,因为数据源可能已经更新。使用稳定的 model ID 判断返回结果是否仍属于当前 cell,通常更安全。
控制布局复杂度
Auto Layout 并不等于性能差,但重复的约束变更、模糊的垂直尺寸和深层视图树会放大滚动成本。
可以依次检查:
- 约束是否只创建一次,而不是在
configure中反复激活; - 是否存在不必要的嵌套 Stack View;
- 圆角、阴影和遮罩是否触发昂贵的离屏渲染;
- 动态高度能否缓存,或者由确定数据直接计算;
- 不可见的复杂子视图是否仍参与布局。
先用 Instruments 证明布局是热点,再考虑手写 frame。过早替换布局系统,会增加维护成本,却未必解决真正的瓶颈。
审视数据更新方式
同一批数据如果拆成多次快照更新,会重复触发布局与动画。Diffable Data Source 也不是免费的,快照构造、差异计算和视图更新都需要成本。
尽量在后台准备不可变的展示模型,再在主线程一次提交:
let viewModels = await formatter.makeViewModels(from: response)
await MainActor.run {
dataSource.apply(makeSnapshot(viewModels), animatingDifferences: false)
}
滚动过程中收到高频状态变化时,可以合并更新,或者只更新真正受影响的 item。是否开启动画应由交互价值决定,而不是默认始终为 true。
用数据验证每一次修改
每次只改一个可解释因素,然后重新执行相同场景。记录修改前后的帧时间、主线程占用和内存,而不是只凭手感。
最终应能回答三个问题:
- 卡顿发生在哪段时间;
- 那段时间主线程在做什么;
- 修改为什么减少了这项工作。
当答案都清楚时,性能优化才从偶然试错变成了可维护的工程结论。