You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

频繁更新UICollectionView数据致滚动卡顿,如何优化?

UICollectionView滚动时更新卡顿问题优化

问题背景

我有一个UICollectionView,每隔约1秒通过WebSocket事件触发数据更新,滚动该集合视图时,每次更新都会明显出现卡顿。使用的技术栈如下:

  • Combine Publishers(用于数据传输)
  • UICollectionViewCompositionalLayout(用于UI布局)
  • UICollectionViewDiffableDataSource(作为UICollectionView的数据源)
  • 每个单元格对应一个持有数据的ViewModel实例

当前的单元格更新处理代码:

viewModel.titleSubject
   .sink { [weak self] title in
                guard let _ = self?.dataSource?.snapshot().indexOfSection(0) else { return }
                let itemIDs = self?.dataSource?.snapshot().itemIdentifiers(inSection: 0)
                if let header = itemIDs?.first as? HeaderCellViewModel {
                    guard header.text != title else { return }
                    header.text = title
                    if var snapshot = self?.dataSource?.snapshot() {
                        snapshot.reloadItems([header])
                        self?.dataSource?.apply(snapshot)
                    }
                }
            }
   .store(in: &cancelBag)

触发更新的代码(未自行处理线程/队列):

titleSubject.send("Hey, I'm a new title")

优化方案

1. 强制UI更新在主线程执行

WebSocket事件通常在后台线程触发,Combine的sink默认会沿用事件发送线程执行,直接在后台操作UI会导致主线程阻塞卡顿。通过receive(on:)切换到主线程处理UI相关操作:

viewModel.titleSubject
   .receive(on: DispatchQueue.main) // 切换到主线程处理UI更新
   .sink { [weak self] title in
                guard let self = self else { return }
                guard let dataSource = self.dataSource,
                      var snapshot = dataSource.snapshot(),
                      snapshot.indexOfSection(0) != NSNotFound,
                      let itemIDs = snapshot.itemIdentifiers(inSection: 0) as? [HeaderCellViewModel],
                      let header = itemIDs.first else { return }
                
                guard header.text != title else { return }
                header.text = title
                snapshot.reloadItems([header])
                dataSource.apply(snapshot)
            }
   .store(in: &cancelBag)

2. 关闭滚动时的更新动画

滚动状态下执行单元格更新动画会额外消耗主线程资源,判断集合视图滚动状态,动态关闭非必要动画:

// 在apply快照时添加判断
let shouldAnimate = !self.collectionView.isDragging && !self.collectionView.isDecelerating
dataSource.apply(snapshot, animatingDifferences: shouldAnimate)

3. 让单元格直接订阅ViewModel更新(最核心优化)

避免通过DiffableDataSource重新加载整个item,改为在单元格内部订阅ViewModel的状态变化,直接更新UI元素:

// 单元格代码示例
class HeaderCell: UICollectionViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    private var cancelBag = Set<AnyCancellable>()
    
    func configure(with viewModel: HeaderCellViewModel) {
        viewModel.titleSubject
            .receive(on: DispatchQueue.main)
            .assign(to: \.text, on: titleLabel)
            .store(in: &cancelBag)
    }
}

这种方式彻底跳过了DiffableDataSource的快照更新流程,完全消除了更新带来的主线程开销。

4. 合并高频更新(可选)

如果WebSocket短时间内触发多次更新,用Combine的throttle或debounce合并事件,减少UI更新频率:

viewModel.titleSubject
   .throttle(for: .milliseconds(500), scheduler: DispatchQueue.main, latest: true) // 500ms内仅保留最新值
   .receive(on: DispatchQueue.main)
   .sink { /* 处理逻辑 */ }
   .store(in: &cancelBag)

5. 减少快照重复查询开销

优化代码中的可选链和重复查询逻辑,提前解包必要对象,避免多层嵌套的可选判断:

// 优化后的sink闭包逻辑
guard let self = self,
      let dataSource = self.dataSource,
      var snapshot = dataSource.snapshot(),
      let header = snapshot.itemIdentifiers(inSection: 0).first as? HeaderCellViewModel else { return }

内容的提问来源于stack exchange,提问作者Elio_

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 17:40:22