UITableView 与 UICollectionView
问题
UITableView 的 Cell 复用原理是什么?如何优化列表性能?DiffableDataSource 解决了什么问题?
答案
Cell 复用原理
// 注册(viewDidLoad 中)
tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")
// 复用
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
cell.configure(with: data[indexPath.row])
return cell
}
复用导致的问题
Cell 被复用时,上一次的状态(图片、选中状态等)可能残留。必须在 cellForRowAt 或 prepareForReuse() 中重置所有 UI 状态。
性能优化
| 优化手段 | 说明 |
|---|---|
| 高度缓存 | 预计算并缓存 Cell 高度,避免重复计算 |
| 预加载 | UITableViewDataSourcePrefetching 提前加载数据 |
| 减少层级 | 减少 Cell 中的视图层级和透明度 |
| 异步绘制 | 复杂内容在后台线程绘制为 Bitmap |
| 避免离屏渲染 | 不要同时设 cornerRadius + masksToBounds |
| 图片异步加载 | 滑动时暂停加载,停下时恢复 |
// 高度缓存
var heightCache: [IndexPath: CGFloat] = [:]
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return heightCache[indexPath] ?? 60
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
heightCache[indexPath] = cell.frame.height
}
// Prefetching
extension VC: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
loadImage(for: data[indexPath.row])
}
}
}
DiffableDataSource
传统 reloadData 的问题:无动画、全量刷新。DiffableDataSource 基于数据快照自动计算差异并执行动画更新:
// 1. 定义 Section 和 Item
enum Section { case main }
struct Item: Hashable {
let id: UUID
let title: String
}
// 2. 创建 DataSource
var dataSource: UITableViewDiffableDataSource<Section, Item>!
dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = item.title
return cell
}
// 3. 应用快照更新
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: true)
UICollectionView Compositional Layout
iOS 13+ 的新布局 API,取代传统 UICollectionViewFlowLayout:
// Section → Group → Item 三层结构
let layout = UICollectionViewCompositionalLayout { sectionIndex, env in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(200))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous // 横向滚动
return section
}
常见面试问题
Q1: dequeueReusableCell(withIdentifier:for:) 和不带 for: 的区别?
答案:
- 带
for::如果没有可复用的 Cell,系统自动创建新的(需要提前 register) - 不带
for::返回nil时需要手动创建
推荐用带 for: 的版本,配合 register 使用。
Q2: UITableView 滑动卡顿怎么排查和优化?
答案:
- Instruments - Time Profiler:找到 cellForRowAt 中的耗时操作
- 避免主线程阻塞:图片解码、数据处理放后台线程
- 高度缓存:避免 AutoLayout 重复计算
- 减少透明和离屏渲染:用 Color Blended Layers 检查
- 图片降采样:大图展示前先等比缩小到实际显示尺寸
Q3: DiffableDataSource 相比传统 DataSource 的优势?
答案:
- 自动计算数据差异,无需手动
insertRows/deleteRows - 避免 "number of rows mismatch" 崩溃
- 内置动画
- 线程安全(可以在后台线程构建 snapshot)
- Item 需要 Hashable,强制数据唯一性