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
相关产品推荐
相关产品推荐

