下载图片时取消URLSessionDataTask的正确方式及优化建议
问题解答
一、当前取消请求的方式是否正确?
你的实现有一定效果,但存在明显缺陷:
- 有效点:当cell滚出屏幕触发
prepareForReuse时,确实会取消当前cell关联的下载任务,避免无用请求占用资源。 - 缺陷:
- 每次
configure都新建ImageDownloader实例,造成不必要的对象创建和内存开销。 - 如果cell在滚出屏幕前被重新配置(比如快速滚动时,cell还在屏幕内就被分配给新的数据源项),之前的下载任务不会被取消,会导致多个并发请求同时运行,浪费带宽和CPU资源。
- 没有校验请求完成后cell是否已被复用,可能出现“旧图片显示在新cell上”的错误。
- 每次
二、替代方案
1. 单例下载器+任务管理
将ImageDownloader设计为单例,维护一个字典记录每个URL对应的下载任务和回调,避免重复请求,同时支持精准取消:
class ImageDownloader { static let shared = ImageDownloader() private let cache = NSCache<NSURL, UIImage>() private var tasks: [URL: URLSessionDataTask] = [:] private var completions: [URL: [(UIImage?) -> Void]] = [:] private let queue = DispatchQueue(label: "com.your.app.imagedownloader") private init() {} func downloadImage(with urlString: String, completion: @escaping (UIImage?) -> Void) { guard let url = URL(string: urlString) else { completion(nil) return } queue.sync { // 优先取缓存 if let cachedImage = cache.object(forKey: url as NSURL) { DispatchQueue.main.async { completion(cachedImage) } return } // 已有任务则追加回调,不重复发起请求 if let existingCompletions = completions[url] { completions[url] = existingCompletions + [completion] return } // 创建新任务 let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } var image: UIImage? defer { self.queue.sync { self.tasks.removeValue(forKey: url) let callbacks = self.completions.removeValue(forKey: url) ?? [] DispatchQueue.main.async { callbacks.forEach { $0(image) } } if let image = image { self.cache.setObject(image, forKey: url as NSURL) } } } if let error = error, (error as NSError).code == NSURLErrorCancelled { return // 取消的任务不回调 } guard let data = data, let img = UIImage(data: data) else { return } image = img } tasks[url] = task completions[url] = [completion] task.resume() } } func cancelDownload(for urlString: String) { guard let url = URL(string: urlString) else { return } queue.sync { tasks[url]?.cancel() tasks.removeValue(forKey: url) completions.removeValue(forKey: url) } } }
2. 使用Combine框架(iOS 13+)
利用URLSession.dataTaskPublisher创建请求,通过AnyCancellable管理订阅生命周期,取消更简洁:
import Combine class CustomCollectionViewCell: UICollectionViewCell { @IBOutlet weak var cellImage: UIImageView! private var cancellable: AnyCancellable? private let cache = NSCache<NSURL, UIImage>() func configure(image: ImageModel) { cancellable?.cancel() // 取消之前的请求 cellImage.image = nil guard let urlString = image.urls.full, let url = URL(string: urlString) else { return } // 检查缓存 if let cachedImage = cache.object(forKey: url as NSURL) { cellImage.image = cachedImage return } cancellable = URLSession.shared.dataTaskPublisher(for: url) .map { UIImage(data: $0.data) } .replaceError(with: nil) .receive(on: DispatchQueue.main) .sink { [weak self] image in guard let self = self, let image = image else { return } self.cellImage.image = image self.cache.setObject(image, forKey: url as NSURL) } } override func prepareForReuse() { super.prepareForReuse() cancellable?.cancel() cellImage.image = nil } }
3. 使用Async/Await(iOS 15+)
通过Task管理异步请求,取消时直接终止Task,底层会自动取消URLSession任务:
class CustomCollectionViewCell: UICollectionViewCell { @IBOutlet weak var cellImage: UIImageView! private var downloadTask: Task<Void, Never>? private let cache = NSCache<NSURL, UIImage>() func configure(image: ImageModel) { downloadTask?.cancel() // 取消之前的任务 cellImage.image = nil guard let urlString = image.urls.full, let url = URL(string: urlString) else { return } if let cachedImage = cache.object(forKey: url as NSURL) { cellImage.image = cachedImage return } downloadTask = Task { do { let (data, _) = try await URLSession.shared.data(from: url) guard let image = UIImage(data: data), !Task.isCancelled else { return } DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.cellImage.image = image self.cache.setObject(image, forKey: url as NSURL) } } catch { if (error as NSError).code != NSURLErrorCancelled { print(error.localizedDescription) } } } } override func prepareForReuse() { super.prepareForReuse() downloadTask?.cancel() cellImage.image = nil } }
4. 第三方成熟库
直接使用Kingfisher、SDWebImage等第三方库,这些库已经封装了图片下载、缓存、请求取消、内存优化等全套逻辑,稳定且省心。
三、为什么要包装DispatchGroup或DispatchWorkItem?
对于单个图片下载任务来说,完全不需要:
DispatchGroup的作用是等待多个任务全部完成后执行统一操作(比如批量下载图片后更新UI),单个任务用它纯属多余。DispatchWorkItem是对任务的包装,支持在队列中取消,但URLSessionDataTask本身已经提供了cancel()方法,直接调用就能终止网络请求、释放资源,包装WorkItem反而会增加复杂度,且无法真正终止底层网络请求。
四、无缓存情况下的流畅加载技巧
- 请求合适尺寸的图片:利用Unsplash API的
w/h参数指定图片尺寸(比如?w=300&h=300),避免下载远超cell显示需求的大图,减少下载时间和内存占用。 - 后台解码图片:
UIImage(data: data)默认在主线程解码,会阻塞UI,放到后台队列处理:DispatchQueue.global(qos: .userInitiated).async { guard let image = UIImage(data: data) else { return } DispatchQueue.main.async { completion(image) } } - 限制并发请求数:通过URLSessionConfiguration设置并发数,避免同时发起过多请求导致网络拥堵:
let config = URLSessionConfiguration.default config.httpMaximumConnectionsPerHost = 4 // 建议设置4-6 let session = URLSession(configuration: config) - 预加载机制:根据滚动方向,提前发起即将进入屏幕的cell的图片请求。
- 内存预警处理:在收到内存警告时,主动清理不再使用的图片对象,避免内存飙升。
- 分阶段加载:先下载低分辨率缩略图占位,再加载高清图,提升用户感知的流畅度。
内容的提问来源于stack exchange,提问作者Osama Naeem
相关产品推荐
相关产品推荐

