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

下载图片时取消URLSessionDataTask的正确方式及优化建议

问题解答

一、当前取消请求的方式是否正确?

你的实现有一定效果,但存在明显缺陷:

  • 有效点:当cell滚出屏幕触发prepareForReuse时,确实会取消当前cell关联的下载任务,避免无用请求占用资源。
  • 缺陷:
    1. 每次configure都新建ImageDownloader实例,造成不必要的对象创建和内存开销。
    2. 如果cell在滚出屏幕前被重新配置(比如快速滚动时,cell还在屏幕内就被分配给新的数据源项),之前的下载任务不会被取消,会导致多个并发请求同时运行,浪费带宽和CPU资源。
    3. 没有校验请求完成后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反而会增加复杂度,且无法真正终止底层网络请求。

四、无缓存情况下的流畅加载技巧

  1. 请求合适尺寸的图片:利用Unsplash API的w/h参数指定图片尺寸(比如?w=300&h=300),避免下载远超cell显示需求的大图,减少下载时间和内存占用。
  2. 后台解码图片:UIImage(data: data)默认在主线程解码,会阻塞UI,放到后台队列处理:
    DispatchQueue.global(qos: .userInitiated).async {
        guard let image = UIImage(data: data) else { return }
        DispatchQueue.main.async { completion(image) }
    }
    
  3. 限制并发请求数:通过URLSessionConfiguration设置并发数,避免同时发起过多请求导致网络拥堵:
    let config = URLSessionConfiguration.default
    config.httpMaximumConnectionsPerHost = 4 // 建议设置4-6
    let session = URLSession(configuration: config)
    
  4. 预加载机制:根据滚动方向,提前发起即将进入屏幕的cell的图片请求。
  5. 内存预警处理:在收到内存警告时,主动清理不再使用的图片对象,避免内存飙升。
  6. 分阶段加载:先下载低分辨率缩略图占位,再加载高清图,提升用户感知的流畅度。

内容的提问来源于stack exchange,提问作者Osama Naeem

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 07:20:27