iOS 列表渲染与滚动性能优化
iOS 列表渲染与滚动性能优化
UITableView 和 UICollectionView 都建立在视图复用机制上。大多数滚动卡顿并非复用失效,而是单元格配置阶段执行了过多主线程工作,或者数据更新范围过大。优化时应该将数据准备、布局计算、图片处理和视图绑定分别观察,找到真正耗时的环节。
一、保持单元格配置轻量
单元格的职责应集中在视图赋值。日期解析、价格格式化、富文本拼装和复杂条件判断,可以提前放到展示模型中完成。
struct QuoteCellModel: Hashable {
let id: String
let name: String
let priceText: String
let changeText: String
let changeColor: UIColor
}
final class QuoteCell: UICollectionViewCell {
func configure(with model: QuoteCellModel) {
nameLabel.text = model.name
priceLabel.text = model.priceText
changeLabel.text = model.changeText
changeLabel.textColor = model.changeColor
}
}
当展示规则集中在模型转换层后,单元格复用时更不容易漏掉某个属性,也便于测试涨跌颜色、占位文案等规则。
二、彻底重置可复用状态
复用问题常表现为图片串位、隐藏状态残留、旧任务回调覆盖新内容。除了在 prepareForReuse 中重置视觉状态,还要取消异步任务。
final class AvatarCell: UICollectionViewCell {
private var imageTask: Task<Void, Never>?
override func prepareForReuse() {
super.prepareForReuse()
imageTask?.cancel()
imageTask = nil
avatarView.image = placeholderImage
nameLabel.text = nil
}
func configure(with model: UserCellModel) {
nameLabel.text = model.name
imageTask = Task { [weak self] in
guard let image = await imageLoader.load(model.avatarKey) else {
return
}
guard !Task.isCancelled else { return }
await MainActor.run {
self?.avatarView.image = image
}
}
}
}
图片组件应优先复用项目已有实现,由它统一管理缓存、尺寸采样和请求合并。
三、使用差量数据源
整表刷新会触发大范围布局和绑定,还会打断部分交互状态。差量数据源通过稳定标识计算插入、删除和移动,更适合常规列表更新。
enum Section: Hashable {
case main
}
func apply(items: [QuoteCellModel], animated: Bool) {
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections([.main])
snapshot.appendItems(items.map(\.id), toSection: .main)
dataSource.apply(snapshot, animatingDifferences: animated)
models = Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })
}
标识必须稳定且唯一。不要使用当前数组下标作为标识,否则插入一项后,大量元素都会被误判为新对象。
四、减少自动布局的重复计算
自适应高度能简化动态内容布局,但约束冲突、约束数量过多和高度频繁变化都会增加成本。建议遵循以下原则:
- 单元格内部约束形成完整且唯一的尺寸链路。
- 不在滚动绑定过程中反复添加和删除约束。
- 对固定结构复用同一组约束,只改变常量或显隐状态。
- 为估算高度提供接近真实内容的值,减少滚动条跳动。
- 确认性能瓶颈后,再考虑缓存昂贵的布局结果。
手动布局并不必然优于自动布局。结构清晰、约束稳定的单元格通常已经足够流畅。
五、控制图片解码与尺寸
高分辨率图片即使文件体积不大,解码后的内存占用也可能很高。列表只需要缩略图时,应按目标显示尺寸进行降采样,并尽可能把解码放到后台执行。
圆角、模糊和渐变等效果也要注意成本。简单圆角可以使用图层裁剪;复杂遮罩如果在大量单元格中实时计算,可能造成离屏渲染。是否需要预处理,应通过实际测量决定。
六、预取不是越多越好
列表预取能够提前准备即将显示的数据,但快速滚动时也会产生许多随后被取消的任务。预取实现需要支持取消,并避免同一个资源重复请求。
func collectionView(
_ collectionView: UICollectionView,
prefetchItemsAt indexPaths: [IndexPath]
) {
let keys = indexPaths.compactMap { model(at: $0)?.imageKey }
imageLoader.prefetch(keys)
}
func collectionView(
_ collectionView: UICollectionView,
cancelPrefetchingForItemsAt indexPaths: [IndexPath]
) {
let keys = indexPaths.compactMap { model(at: $0)?.imageKey }
imageLoader.cancelPrefetch(keys)
}
短列表或纯文本列表通常不需要额外预取逻辑。
七、合并高频界面刷新
行情、倒计时等数据可能频繁变化。业务层可以完整处理每条数据,而展示层按允许的节奏合并刷新。对于可见单元格,只更新真正变化的标签,比持续重建整个快照更经济。
合并周期应根据业务精度设定。如果数据代表交易确认、支付结果等关键状态,就不能为了流畅而延迟或丢弃。
八、用数据验证结果
性能优化至少要同时观察以下维度:
- 主线程单帧耗时和掉帧位置。
- 单元格创建次数与绑定耗时。
- 布局计算和图片解码时间。
- 快速滚动后的内存峰值。
- 数据更新时是否出现明显停顿。
测试应使用稳定数据量、固定操作路径和同一设备条件。一次只调整一个主要因素,才能判断改动是否真正有效。
总结
iOS 列表优化的本质是控制主线程工作量。提前构建展示模型、正确清理复用状态、使用稳定标识做差量更新、按目标尺寸处理图片,并根据业务节奏合并高频刷新,可以解决绝大多数常见卡顿。优化结论应来自测量,而不是来自复杂度本身。
- 点赞
- 收藏
- 关注作者
评论(0)