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

百万级数据UITableView滚动不卡顿优化及刷新时机咨询

解决UITableView滚动卡顿+合理刷新的方案

一、先排查核心卡顿原因

用DiffableDataSource还卡,大概率不是数据源类型的问题,先盯这几个关键点:

  • 回调是不是在后台线程触发的?直接在后台更新model或刷新UI必然卡顿
  • 生成数据的百万次循环有没有占用主线程?主线程被循环堵死,滚动肯定受影响
  • Cell渲染是不是太繁重?比如存在复杂计算、异步加载没做优化,或者复用逻辑有问题

二、具体实现步骤

1. 强制所有UI相关操作在主线程执行

不管回调本身是否在主线程,都要把更新model、刷新数据源的逻辑切到主线程,先改你的回调代码:

server.subscribeToDeals { deals in
    DispatchQueue.main.async {
        self.model.append(contentsOf: deals)
        // 快照更新逻辑放在这里,后面说时机
    }
}

2. 把百万次循环扔到后台线程

生成数据的绝对不能占主线程,直接丢到全局后台队列执行:

func generateDeals() {
    DispatchQueue.global(qos: .background).async {
        var tempDeals = [Deal]()
        for _ in 0..<1_000_000 {
            let deal = Deal(...) // 生成单条数据
            tempDeals.append(deal)
            // 每攒100条就回调主线程
            if tempDeals.count == 100 {
                DispatchQueue.main.async {
                    self.server.sendDeals(tempDeals) // 触发回调的方法
                }
                tempDeals.removeAll()
            }
        }
        // 处理最后不足100条的剩余数据
        if !tempDeals.isEmpty {
            DispatchQueue.main.async {
                self.server.sendDeals(tempDeals)
            }
        }
    }
}

后台线程的优先级低于用户交互,系统会优先处理UITableView的滚动事件,不会被循环阻塞。

3. 优化DiffableDataSource的刷新时机

别每次收到100条就立刻刷新,滚动时频繁刷新会打断流畅度,做两个优化:

  • 滚动时暂存数据,停止后批量更新:通过UIScrollViewDelegate监听滚动状态,滚动时把新数据存起来,停止后一次性更新
var isScrolling = false
var pendingDeals = [Deal]()

// 实现UIScrollViewDelegate
extension YourViewController: UIScrollViewDelegate {
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        isScrolling = true
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate {
            isScrolling = false
            processPendingDeals()
        }
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        isScrolling = false
        processPendingDeals()
    }
}

// 批量处理暂存数据
func processPendingDeals() {
    guard !pendingDeals.isEmpty else { return }
    model.append(contentsOf: pendingDeals)
    pendingDeals.removeAll()
    // 基于当前快照追加,比重新创建快照效率更高
    if let currentSnapshot = dataSource.snapshot() {
        var newSnapshot = currentSnapshot
        newSnapshot.appendItems(model.suffix(pendingDeals.count))
        dataSource.apply(newSnapshot, animatingDifferences: false) // 滚动停止后不用动画更流畅
    }
}

// 回调里调整逻辑
server.subscribeToDeals { deals in
    DispatchQueue.main.async {
        if self.isScrolling {
            self.pendingDeals.append(contentsOf: deals)
        } else {
            self.model.append(contentsOf: deals)
            if let currentSnapshot = self.dataSource.snapshot() {
                var newSnapshot = currentSnapshot
                newSnapshot.appendItems(deals)
                self.dataSource.apply(newSnapshot, animatingDifferences: true) // 非滚动时可以加动画
            }
        }
    }
}
  • 合并高频回调:如果回调频率过高(比如1秒多次),可以用定时器合并300ms内的所有新数据,一次性更新,避免频繁触发UI刷新。

4. 优化Cell渲染性能

很多时候滚动卡顿不是数据源的问题,是Cell拖了后腿:

  • 提前计算Cell高度,用UITableViewAutomaticDimension时给estimatedRowHeight设置接近实际的值,或者缓存高度
  • 圆角、阴影这类绘制操作提前用CAShapeLayer预渲染,避免在Cell复用时重复计算
  • Cell内的图片用异步加载+缓存,绝对不要在cellForRowAt里同步加载
  • 不在cellForRowAt里做任何耗时操作,所有数据提前处理好直接赋值给Cell

三、DiffableDataSource的小技巧

别每次都创建全新快照,基于当前快照追加数据,能减少差异计算的开销:

if let currentSnapshot = dataSource.snapshot() {
    var newSnapshot = currentSnapshot
    newSnapshot.appendItems(deals)
    dataSource.apply(newSnapshot, animatingDifferences: !isScrolling)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 21:45:39