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

如何异步加载UICollectionViewCell多图且避免复用问题与卡顿?

优化自定义绘制Cell的多图加载方案(解决滚动卡顿与复用问题)

针对你这种通过draw(_ rect:)自定义绘制UICollectionViewCell、同时需要加载多张图片的场景,结合你后来提到的预加载思路,我整理几个更完善的优化方案,解决当前方案的滚动卡顿和图片切换问题:

一、先分析当前方案的核心问题

你现在在willDisplayCell里用DispatchGroup批量加载图片的方式,主要有两个痛点:

  1. 复用冲突:滚动时cell会被复用,cachedIndexPath可能已经对应新的cell,导致旧请求的图片错误更新到新cell上;
  2. 主线程阻塞:批量请求的回调都在主线程处理,加上图片绘制的开销,容易造成滚动卡顿;
  3. 无请求取消机制: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 06:55:30