如何异步加载UICollectionViewCell多图且避免复用问题与卡顿?
优化自定义绘制Cell的多图加载方案(解决滚动卡顿与复用问题)
针对你这种通过draw(_ rect:)自定义绘制UICollectionViewCell、同时需要加载多张图片的场景,结合你后来提到的预加载思路,我整理几个更完善的优化方案,解决当前方案的滚动卡顿和图片切换问题:
一、先分析当前方案的核心问题
你现在在willDisplayCell里用DispatchGroup批量加载图片的方式,主要有两个痛点:
- 复用冲突:滚动时cell会被复用,
cachedIndexPath可能已经对应新的cell,导致旧请求的图片错误更新到新cell上; - 主线程阻塞:批量请求的回调都在主线程处理,加上图片绘制的开销,容易造成滚动卡顿;
- 无请求取消机制:cell复用时未取消旧的图片请求,无效请求会持续占用网络和内存资源。
二、优化方案
1. 将图片加载逻辑封装到Cell内部,避免复用冲突
把图片的请求管理、状态清理都放到自定义Cell里,利用prepareForReuse方法处理复用场景,从根源上解决旧请求干扰新cell的问题:
class ListCollectionViewCell: UICollectionViewCell { var posters: [UIImage] = [] { didSet { setNeedsDisplay() } } // 保存当前cell的所有图片下载任务,方便复用时取消 private var imageDownloadTasks: [DownloadTask?] = [] // 对外暴露的图片加载方法 func loadPosters(with providers: [ImageProvider?]) { // 复用/重新加载前,先清理旧任务和图片 imageDownloadTasks.forEach { $0?.cancel() } imageDownloadTasks.removeAll() posters.removeAll() setNeedsDisplay() let loadGroup = DispatchGroup() // 过滤空的provider,发起图片请求 providers.compactMap { $0 }.forEach { provider in loadGroup.enter() let task = KingfisherManager.shared.retrieveImage( with: .provider(provider), options: [.targetCache(posterCache), .processor(DownsamplingImageProcessor(size: CGSize(width: yourTargetWidth, height: yourTargetHeight)))] ) { [weak self] result in defer { loadGroup.leave() } guard let self = self else { return } DispatchQueue.main.async { switch result { case .success(let imageResult): self.posters.append(imageResult.image) case .failure: print("Failed to load poster") } } } imageDownloadTasks.append(task) } } override func prepareForReuse() { super.prepareForReuse() // 复用前彻底清理任务和图片 imageDownloadTasks.forEach { $0?.cancel() } imageDownloadTasks.removeAll() posters.removeAll() setNeedsDisplay() } override func draw(_ rect: CGRect) { super.draw(rect) // 你的自定义绘制逻辑 guard let context = UIGraphicsGetCurrentContext() else { return } posters.enumerated().forEach { index, image in let posterFrame = // 根据index计算每个图片的frame context.draw(image, in: posterFrame) } } }
然后在ViewController的cellForItemAt里直接调用Cell的加载方法即可:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ListCell", for: indexPath) as! ListCollectionViewCell let imageProviders = viewModel.imageProviders(at: indexPath) cell.loadPosters(with: imageProviders) return cell }
2. 结合UICollectionViewDataSourcePrefetching预加载图片
你提到的预加载思路非常正确,通过预加载把图片提前缓存到本地,滚动时直接读取缓存,能大幅减少实时加载的卡顿:
class YourViewController: UIViewController, UICollectionViewDataSourcePrefetching { override func viewDidLoad() { super.viewDidLoad() collectionView.prefetchDataSource = self } // 预加载指定indexPath的图片(仅缓存,不更新UI) func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { indexPaths.forEach { indexPath in let providers = viewModel.imageProviders(at: indexPath) providers.compactMap { $0 }.forEach { provider in KingfisherManager.shared.retrieveImage( with: .provider(provider), options: [.targetCache(posterCache), .onlyFromCacheOrRefresh] ) { _ in } // 只做缓存,不处理回调 } } } // 取消不再需要的预加载请求(比如用户快速滚动跳过了某些cell) func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { indexPaths.forEach { indexPath in let providers = viewModel.imageProviders(at: indexPath) providers.compactMap { $0 }.forEach { provider in KingfisherManager.shared.downloader.cancelTask(for: .provider(provider)) } } } }
3. 额外优化:减少主线程绘制压力
- 用Kingfisher的
processor参数,在后台提前处理图片尺寸(比如上面代码里的DownsamplingImageProcessor),避免在主线程做图片缩放; - 如果图片数量固定,可以一次性收集完所有加载好的图片再赋值给
posters,减少setNeedsDisplay的触发次数(比如在loadGroup.notify里统一赋值)。
三、最终效果
结合Cell内部的请求管理+预加载缓存,既能解决滚动时的卡顿问题,又能彻底避免cell复用导致的图片错乱,同时还能有效控制API调用频率(预加载可以限制每次预加载的数量,比如只预加载当前可见cell前后3个位置的内容)。
内容的提问来源于stack exchange,提问作者Maximilian Litteral
相关产品推荐
相关产品推荐

