百万级数据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
相关产品推荐
相关产品推荐

