Make blazing fast lists and collection views
Swift & UI 进阶 20m

打造极速列表和集合视图

Make blazing fast lists and collection views

2021年6月10日

在 Apple 官方观看视频

一句话判断

UICollectionView 的 cell prefetching 和差异更新(Diffable Data Source)在 iOS 15 中做了性能优化——但真正的性能飞跃来自正确使用 cell registration 和避免在 cellForItemAt 中做布局计算。

这场 Session 讲了什么

Session 深入讲解了 UICollectionView 和 UITableView 的性能优化技巧。iOS 15 在底层做了大量优化:cell 预取(Prefetching)更智能,Diffable Data Source 的差异计算更快,SwiftUI 的 List 底层重写后性能接近 UIKit。

但 Session 的核心信息是:大部分列表性能问题不是系统慢,而是你的代码慢。常见的性能杀手包括:在 cellForItemAt 中做耗时操作(网络请求、图片解码、复杂布局计算)、不正确的 cell 复用(每次创建新 cell 而不是复用)、过高的 cell 复杂度(一个 cell 包含太多子视图)。

Session 逐个演示了用 Instruments 的 Time Profiler 和 SwiftUI View Debugger 定位列表瓶颈的方法,以及修复后的效果对比。

值得深挖的点

Cell Registration 的正确方式

UICollectionView.CellRegistration 是 iOS 14 引入的 cell 注册方式,但很多项目还在用 registerNibdequeueReusableCell(withReuseIdentifier:for:) 的旧模式。新方式的好处是:cell 的配置逻辑被集中在一个闭包中,系统可以更好地优化复用和预取。配合 UICollectionViewDiffableDataSource,你不再需要写 numberOfItemsInSectioncellForItemAt——数据源驱动一切。

图片解码是最大的性能杀手

如果你的列表 cell 包含网络图片,而你在主线程做图片解码和解压缩,列表滚动就会卡顿。Session 展示了正确的做法:在后台线程解码图片(UIImage(data:) 默认是延迟解码的,只有在渲染时才解码),然后缓存解码后的 CGImage。Kingfisher 和 SDWebImage 等库已经帮你做了这件事。

代码片段

使用 Cell Registration 和 Diffable Data Source

// 创建 Cell Registration
let cellRegistration = UICollectionView.CellRegistration<PhotoCell, Photo> { cell, indexPath, photo in
    // 只做轻量的 UI 配置
    cell.titleLabel.text = photo.title
    cell.dateLabel.text = photo.dateFormatted
    
    // 异步加载图片
    cell.loadImage(from: photo.thumbnailURL)
}

// 创建 Diffable Data Source
let dataSource = UICollectionViewDiffableDataSource<Section, Photo>(
    collectionView: collectionView
) { collectionView, indexPath, photo in
    // 使用 registration 出列 cell
    collectionView.dequeueConfiguredReusableCell(
        using: cellRegistration, for: indexPath, item: photo
    )
}

// 更新数据(差异计算在后台自动完成)
var snapshot = NSDiffableDataSourceSnapshot<Section, Photo>()
snapshot.appendSections([.main])
snapshot.appendItems(photos)
dataSource.apply(snapshot, animatingDifferences: true)

后台解码图片

extension UIImage {
    // 在后台线程预解码图片
    static func predecodedImage(from data: Data) -> UIImage? {
        guard let cgImage = UIImage(data: data)?.cgImage else { return nil }
        
        // 创建一个与屏幕色彩空间匹配的 bitmap context
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        guard let context = CGContext(
            data: nil,
            width: cgImage.width,
            height: cgImage.height,
            bitsPerComponent: 8,
            bytesPerRow: cgImage.width * 4,
            space: colorSpace,
            bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
        ) else { return nil }
        
        // 绘制到 context,触发解码
        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
        
        // 从 context 获取预解码的图片
        guard let decodedImage = context.makeImage() else { return nil }
        return UIImage(cgImage: decodedImage)
    }
}

自适应 Cell 高度的优化

// 使用 self-sizing cell 时,系统会多次计算高度
// 缓存计算结果避免重复计算
class PhotoCell: UICollectionViewCell {
    static var heightCache: [String: CGFloat] = [:]
    
    func configure(with photo: Photo) {
        // 如果已经计算过高度,直接用缓存
        if let cachedHeight = Self.heightCache[photo.id] {
            contentView.heightAnchor.constraint(equalToConstant: cachedHeight).isActive = true
        }
        
        // 配置内容...
        titleLabel.text = photo.title
    }
    
    override func preferredLayoutAttributesFitting(
        _ layoutAttributes: UICollectionViewLayoutAttributes
    ) -> UICollectionViewLayoutAttributes {
        let attrs = super.preferredLayoutAttributesFitting(layoutAttributes)
        // 缓存计算结果
        Self.heightCache[currentPhotoID] = attrs.frame.height
        return attrs
    }
}

最佳实践

性能检查清单: 用 Instruments 的 Time Profiler 跑一遍你的列表滚动。如果在 cellForItemAtconfigure 闭包中看到耗时超过 1ms 的操作,那就是瓶颈。常见的修复:图片后台解码、预计算 cell 高度、减少 cell 子视图数量。

SwiftUI List: 如果你用 SwiftUI 的 List,iOS 15 的性能已经接近 UIKit。关键是避免在 rowbody 中做计算——把计算提到视图外部,用 @State@ObservedObject 缓存结果。

还有什么值得关注

  • UICollectionView.Prefetching 在 iOS 15 中更智能,会根据滚动速度预取更多 cell。
  • UICollectionLayoutSectionOrthogonalScrolling 的性能在 iOS 15 中改善了。
  • SwiftUI 的 LazyVStackLazyHStack 在 iOS 15 中的内存使用更优化。
WWDC 2021