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

UITableViewCell首次加载时URL图片显示错误问题求助

问题场景

为UITableViewCell设置URL图片时出现串位异常:自定义了CancelableImageView(继承自UIImageView),本意是设置新图片时取消当前下载任务,但UITableView中仍存在图片串位问题。

初始加载完成后显示正常,但当第一个单元格的图片加载完成更新后,第四个单元格的图片也会变成第一个单元格的图片,滚动UITableView后第四个单元格才显示正确图片。

图片串位现象

问题分析

问题出在三个核心点:

  • 任务取消逻辑无效:CancelableImageView的cancelLoadingImage方法在全局队列异步执行取消,但DispatchWorkItem的cancel()只是标记任务为取消状态,无法终止已经开始的同步任务(比如loadImage里的try? Data(contentsOf: imageURL)是同步阻塞操作,一旦启动就无法中断)。
  • 复用未彻底清理任务:prepareForReuse仅设置了占位图,但没有主动调用posterImageView.cancelLoadingImage(),也没有清空imageLoadingWorkItem,导致被复用的cell仍在执行之前的加载任务,完成后会覆盖当前正确的图片。
  • 同步加载无法中断:使用Data(contentsOf:)同步加载图片,不仅会阻塞线程,而且完全不支持中途取消,这就导致即使标记了WorkItem取消,旧任务还是会继续执行直到完成,最终错误设置图片。
解决方案

1. 重构图片加载逻辑,改用异步可取消的URLSession请求

替换原有的同步Data(contentsOf:),使用URLSessionDataTask实现真正可取消的异步加载:

extension UIImageView {
    func loadImage(_ urlString: String?, forceOption: Bool = false, completion: @escaping (UIImage?) -> Void) {
        guard let imagePath = urlString else {
            completion(nil)
            return
        }
        
        // 优先读取缓存
        if let cachedImage = ImageCacheManager.shared.loadCachedData(for: imagePath), !forceOption {
            completion(cachedImage)
            return
        }
        
        guard let imageURL = URL(string: imagePath) else {
            completion(nil)
            return
        }
        
        let task = URLSession.shared.dataTask(with: imageURL) { [weak self] data, _, error in
            guard error == nil, let data = data, let newImage = UIImage(data: data) else {
                completion(nil)
                return
            }
            
            // 保存到缓存
            ImageCacheManager.shared.saveCacheData(of: newImage, for: imagePath)
            completion(newImage)
        }
        task.resume()
    }
}

2. 修复CancelableImageView的取消逻辑

用URLSessionDataTask替代DispatchWorkItem,实现真正的任务取消:

class CancelableImageView: UIImageView {
    private var currentTask: URLSessionDataTask?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    init() {
        super.init(frame: .zero)
    }
    
    func setNewImage(_ urlString: String?, forceOption: Bool = false) {
        // 取消当前任务
        cancelLoadingImage()
        
        guard let urlString = urlString else {
            self.image = UIImage(named: "img_placeholder")
            return
        }
        
        self.loadImage(urlString, forceOption: forceOption) { [weak self] image in
            DispatchQueue.main.async {
                self?.image = image ?? UIImage(named: "img_placeholder")
            }
        }
    }
    
    func cancelLoadingImage() {
        currentTask?.cancel()
        currentTask = nil
    }
}

3. 完善Cell的复用清理逻辑

在prepareForReuse中主动取消图片加载任务,确保复用前彻底清理:

class ChartTableViewCell: UITableViewCell {
    lazy var posterImageView = CancelableImageView().then {
        $0.contentMode = .scaleAspectFit
        $0.image = UIImage(named: "img_placeholder")
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        posterImageView.cancelLoadingImage()
        setPosterPlaceholderImage()
    }
    
    private func setPosterPlaceholderImage() {
        self.posterImageView.image = UIImage(named: "img_placeholder")
    }
    
    func setData(rank: Int, movie: MovieFront) {
        rankLabel.text = "\(rank+1)"
        titleLabel.text = movie.title
        genreLabel.text = movie.genre
        releaseDateLabel.text = movie.releaseDate
        starRating.rating = movie.ratingScore/2
        ratingCountLabel.text = "(\(movie.ratingCount))"
        
        if let imagePath = movie.posterPath {
            posterImageView.setNewImage(APIService.configureUrlString(imagePath: imagePath))
        } else {
            posterImageView.image = UIImage(named: "img_placeholder")
        }
    }
}

4. 优化ImageCacheManager(可选)

如果原有的Cache是自定义实现,建议替换为系统的NSCache,更适合图片缓存场景:

class ImageCacheManager {
    private let cache = NSCache<NSString, UIImage>()
    static let shared = ImageCacheManager()
    
    func loadCachedData(for key: String) -> UIImage? {
        return cache.object(forKey: key as NSString)
    }
    
    func saveCacheData(of image: UIImage, for key: String) {
        cache.setObject(image, forKey: key as NSString)
    }
}
核心改进点说明
  • 用URLSessionDataTask替代同步的Data(contentsOf:),实现真正可中断的异步加载,解决了旧任务无法取消的问题。
  • 在prepareForReuse中主动取消加载任务,确保cell复用时不会被旧任务覆盖图片。
  • 取消逻辑改为直接操作URLSessionDataTask,无需依赖DispatchWorkItem的标记取消,可靠性大幅提升。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 12:45:01