有没有办法使用 URLSession.shared.dataTask 并行请求多个不同的资源

Is there a way to request multiple distinct resources in parallel using URLSession.shared.dataTask

我在这里找到了一段关于如何同时下载图像而没有任何破损的代码,

    func loadImageRobsAnswer(with urlString: String?) {
    // cancel prior task, if any


    weak var oldTask = currentTask
    currentTask = nil
    oldTask?.cancel()



    // reset imageview's image

    self.image = nil

    // allow supplying of `nil` to remove old image and then return immediately

    guard let urlString = urlString else { return }

    // check cache



    if let cachedImage = DataCache.shared.object(forKey: urlString) {



        self.transition(toImage: cachedImage as? UIImage)
        //self.image = cachedImage
        return
    }

    // download

    let url = URL(string: urlString)!
    currentURL = url

    let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
        self?.currentTask = nil



        if let error = error {


            if (error as NSError).domain == NSURLErrorDomain && (error as NSError).code == NSURLErrorCancelled {
                return
            }

            print(error)
            return
        }

        guard let data = data, let downloadedImage = UIImage(data: data) else {
            print("unable to extract image")
            return
        }

        DataCache.shared.saveObject(object: downloadedImage, forKey: urlString)

        if url == self?.currentURL {

            DispatchQueue.main.async {

                self?.transition(toImage: downloadedImage)

            }
        }
    }

    // save and start new task

    currentTask = task
    task.resume()
}

然而,此代码用于 UIImageView 扩展,

    public extension UIImageView {
  private static var taskKey = 0
  private static var urlKey = 0

  private var currentTask: URLSessionTask? {
    get { return objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
    set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}

private var currentURL: URL? {
    get { return objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
    set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}}}

这就是我尝试使此代码动态化的方式,因此它不会仅限于一个 UIImageView,而是可用于下载多个资源。

class DataRequest {
private static var taskKey = 0
private static var urlKey = 0
static let shared = DataRequest()
    typealias ImageDataCompletion = (_ image: UIImage?, _ error: Error? ) -> Void

private var currentTask: URLSessionTask? {
    get { return objc_getAssociatedObject(self, &DataRequest.taskKey) as? URLSessionTask }
    set { objc_setAssociatedObject(self, &DataRequest.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}

private var currentURL: URL? {
    get { return objc_getAssociatedObject(self, &DataRequest.urlKey) as? URL }
    set { objc_setAssociatedObject(self, &DataRequest.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}


 func downloadImage(with urlString: String?, completion: @escaping ImageDataCompletion) {



    weak var oldTask = currentTask
    currentTask = nil
    oldTask?.cancel()





    guard let urlString = urlString else { return }





    if let cachedImage = DataCache.shared.object(forKey: urlString) {
         DispatchQueue.main.async {
        completion(cachedImage as? UIImage ,nil)
        }
       // self.transition(toImage: cachedImage as? UIImage)
        //self.image = cachedImage
        return
    }

    // download

    let url = URL(string: urlString)!
    currentURL = url

    let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
        self?.currentTask = nil



        if let error = error {


            if (error as NSError).domain == NSURLErrorDomain && (error as NSError).code == NSURLErrorCancelled {
                return
            }

             completion(nil,nil)
            return
        }

        guard let data = data, let downloadedImage = UIImage(data: data) else {
            print("unable to extract image")
            return
        }

        DataCache.shared.saveObject(object: downloadedImage, forKey: urlString)

        if url == self?.currentURL {

            DispatchQueue.main.async {

                 completion(downloadedImage ,nil)

            }
        }
    }

    // save and start new task

    currentTask = task
    task.resume()
}

所以我现在可以像这样在 UIImageview 扩展中使用它

    extension UIImageView {
       func setImage(url: String?) {

    self.image = nil
    DataRequest.shared.downloadImage(with: url) { (image, error) in
        DispatchQueue.main.async {
            self.image = image


        }
    }

}
    }

结论是,在 UICollectionView 上使用我的方法会在单元格中显示错误的图像并进行复制,我该如何防止这种情况发生?

你问:

Is there a way to request multiple distinct resources in parallel using URLSession.shared.dataTask

默认情况下,它会并行执行请求。

让我们退后一步:在您之前的问题中,您问的是如何实现 Kingfisher-like UIImageView 扩展。在 中,我提到使用 objc_getAssociatedObjectobjc_setAssociatedObject 来实现。但是在你这里的问题中,你已经采用了相关的对象逻辑并将其放入你的 DataRequest 对象中。

您的思考过程,将异步图像检索逻辑从 UIImageView 中提取出来是个好主意:您可能想要为按钮请求图像。你可能有一个通用的“异步获取图像”例程,完全独立于任何 UIKit 对象。因此,从扩展中抽象出网络层代码是一个绝妙的主意。

但是异步图像检索 UIImageView/UIButton 扩展背后的整个想法是我们想要一个 UIKit 控件,它不仅可以执行异步请求,而且如果带有控件的单元格是重用,它将在开始下一个异步请求之前取消先前的异步请求(如果有的话)。这样,如果我们快速向下滚动到图像 80 到 99,对单元格 0 到 79 的请求将被取消,并且可见图像不会积压在所有这些旧图像请求之后。

但要实现这一点,这意味着控件需要某种方式来以某种方式跟踪先前对该重用单元格的请求。因为我们不能在 UIImageView 扩展中添加存储属性,所以我们使用 objc_getAssociatedObjectobjc_setAssociatedObject 模式。但这必须在图像视图中。

不幸的是,在您上面的代码中,关联对象在您的 DataRequest 对象中。首先,正如我试图概述的那样,整个想法是图像视图必须跟踪对该控件的先前请求。将这个“跟踪先前的请求”放在 DataRequest 对象中违背了这个目的。其次,值得注意的是,您不需要自己类型中的关联对象,例如 DataRequest。您只需要存储一个 属性。你只需要在扩展另一个类型时通过这个关联对象愚蠢,例如 UIImageView.

下面是我拼凑的一个简单示例,显示了用于异步图像检索的 UIImageView 扩展。请注意,这并没有从扩展中提取网络代码,但请注意,用于跟踪先前请求的关联对象逻辑必须保留在扩展中。

private var taskKey: Void?

extension UIImageView {
    private static let imageProcessingQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".imageprocessing", attributes: .concurrent)

    private var savedTask: URLSessionTask? {
        get { return objc_getAssociatedObject(self, &taskKey) as? URLSessionTask }
        set { objc_setAssociatedObject(self, &taskKey, newValue, .OBJC_ASSOCIATION_RETAIN) }
    }

    /// Set image asynchronously.
    ///
    /// - Parameters:
    ///   - url: `URL` for image resource.
    ///   - placeholder: `UIImage` of placeholder image. If not supplied, `image` will be set to `nil` while request is underway.
    ///   - shouldResize: Whether the image should be scaled to the size of the image view. Defaults to `true`.

    func setImage(_ url: URL, placeholder: UIImage? = nil, shouldResize: Bool = true) {
        savedTask?.cancel()
        savedTask = nil

        image = placeholder
        if let image = ImageCache.shared[url] {
            DispatchQueue.main.async {
                UIView.transition(with: self, duration: 0.1, options: .transitionCrossDissolve, animations: {
                    self.image = image
                }, completion: nil)
            }
            return
        }

        var task: URLSessionTask!
        let size = bounds.size * UIScreen.main.scale
        task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard
                error == nil,
                let httpResponse = response as? HTTPURLResponse,
                (200..<300) ~= httpResponse.statusCode,
                let data = data
            else {
                return
            }

            UIImageView.imageProcessingQueue.async { [weak self] in
                var image = UIImage(data: data)
                if shouldResize {
                    image = image?.scaledAspectFit(to: size)
                }

                ImageCache.shared[url] = image

                DispatchQueue.main.async {
                    guard
                        let self = self,
                        let savedTask = self.savedTask,
                        savedTask.taskIdentifier == task.taskIdentifier
                    else {
                        return
                    }
                    self.savedTask = nil

                    UIView.transition(with: self, duration: 0.1, options: .transitionCrossDissolve, animations: {
                        self.image = image
                    }, completion: nil)
                }
            }
        }
        task.resume()
        savedTask = task
    }
}

class ImageCache {
    static let shared = ImageCache()

    private let cache = NSCache<NSURL, UIImage>()
    private var observer: NSObjectProtocol?

    init() {
        observer = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: nil) { [weak self] _ in
            self?.cache.removeAllObjects()
        }
    }

    deinit {
        NotificationCenter.default.removeObserver(observer!)
    }

    subscript(url: URL) -> UIImage? {
        get {
            return cache.object(forKey: url as NSURL)
        }

        set {
            if let data = newValue {
                cache.setObject(data, forKey: url as NSURL)
            } else {
                cache.removeObject(forKey: url as NSURL)
            }
        }
    }
}

这是我调整大小的例程:

extension UIImage {

    /// Resize the image to be the required size, stretching it as needed.
    ///
    /// - parameter newSize:      The new size of the image.
    /// - parameter contentMode:  The `UIView.ContentMode` to be applied when resizing image.
    ///                           Either `.scaleToFill`, `.scaleAspectFill`, or `.scaleAspectFit`.
    ///
    /// - returns:                Return `UIImage` of resized image.

    func scaled(to newSize: CGSize, contentMode: UIView.ContentMode = .scaleToFill) -> UIImage? {
        switch contentMode {
        case .scaleToFill:
            return filled(to: newSize)

        case .scaleAspectFill, .scaleAspectFit:
            let horizontalRatio = size.width  / newSize.width
            let verticalRatio   = size.height / newSize.height

            let ratio: CGFloat!
            if contentMode == .scaleAspectFill {
                ratio = min(horizontalRatio, verticalRatio)
            } else {
                ratio = max(horizontalRatio, verticalRatio)
            }

            let sizeForAspectScale = CGSize(width: size.width / ratio, height: size.height / ratio)
            let image = filled(to: sizeForAspectScale)
            let doesAspectFitNeedCropping = contentMode == .scaleAspectFit && (newSize.width > sizeForAspectScale.width || newSize.height > sizeForAspectScale.height)
            if contentMode == .scaleAspectFill || doesAspectFitNeedCropping {
                let subRect = CGRect(
                    x: floor((sizeForAspectScale.width - newSize.width) / 2.0),
                    y: floor((sizeForAspectScale.height - newSize.height) / 2.0),
                    width: newSize.width,
                    height: newSize.height)
                return image?.cropped(to: subRect)
            }
            return image

        default:
            return nil
        }
    }

    /// Resize the image to be the required size, stretching it as needed.
    ///
    /// - parameter newSize:   The new size of the image.
    ///
    /// - returns:             Resized `UIImage` of resized image.

    func filled(to newSize: CGSize) -> UIImage? {
        let format = UIGraphicsImageRendererFormat()
        format.opaque = false
        format.scale = scale

        return UIGraphicsImageRenderer(size: newSize, format: format).image { _ in
            draw(in: CGRect(origin: .zero, size: newSize))
        }
    }

    /// Crop the image to be the required size.
    ///
    /// - parameter bounds:    The bounds to which the new image should be cropped.
    ///
    /// - returns:             Cropped `UIImage`.

    func cropped(to bounds: CGRect) -> UIImage? {
        // if bounds is entirely within image, do simple CGImage `cropping` ...

        if CGRect(origin: .zero, size: size).contains(bounds) {
            return cgImage?.cropping(to: bounds * scale).flatMap {
                UIImage(cgImage: [=11=], scale: scale, orientation: imageOrientation)
            }
        }

        // ... otherwise, manually render whole image, only drawing what we need

        let format = UIGraphicsImageRendererFormat()
        format.opaque = false
        format.scale = scale

        return UIGraphicsImageRenderer(size: bounds.size, format: format).image { _ in
            let origin = CGPoint(x: -bounds.minX, y: -bounds.minY)
            draw(in: CGRect(origin: origin, size: size))
        }
    }

    /// Resize the image to fill the rectange of the specified size, preserving the aspect ratio, trimming if needed.
    ///
    /// - parameter newSize:   The new size of the image.
    ///
    /// - returns:             Return `UIImage` of resized image.

    func scaledAspectFill(to newSize: CGSize) -> UIImage? {
        return scaled(to: newSize, contentMode: .scaleAspectFill)
    }

    /// Resize the image to fit within the required size, preserving the aspect ratio, with no trimming taking place.
    ///
    /// - parameter newSize:   The new size of the image.
    ///
    /// - returns:             Return `UIImage` of resized image.

    func scaledAspectFit(to newSize: CGSize) -> UIImage? {
        return scaled(to: newSize, contentMode: .scaleAspectFit)
    }

    /// Create smaller image from `Data`
    ///
    /// - Parameters:
    ///   - data: The image `Data`.
    ///   - maxSize: The maximum edge size.
    ///   - scale: The scale of the image (defaults to device scale if 0 or omitted.
    /// - Returns: The scaled `UIImage`.

    class func thumbnail(from data: Data, maxSize: CGFloat, scale: CGFloat = 0) -> UIImage? {
        guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else {
            return nil
        }

        return thumbnail(from: imageSource, maxSize: maxSize, scale: scale)
    }

    /// Create smaller image from `URL`
    ///
    /// - Parameters:
    ///   - data: The image file URL.
    ///   - maxSize: The maximum edge size.
    ///   - scale: The scale of the image (defaults to device scale if 0 or omitted.
    /// - Returns: The scaled `UIImage`.

    class func thumbnail(from fileURL: URL, maxSize: CGFloat, scale: CGFloat = 0) -> UIImage? {
        guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
            return nil
        }

        return thumbnail(from: imageSource, maxSize: maxSize, scale: scale)
    }

    private class func thumbnail(from imageSource: CGImageSource, maxSize: CGFloat, scale: CGFloat) -> UIImage? {
        let scale = scale == 0 ? UIScreen.main.scale : scale
        let options: [NSString: Any] = [
            kCGImageSourceThumbnailMaxPixelSize: maxSize * scale,
            kCGImageSourceCreateThumbnailFromImageAlways: true
        ]
        if let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) {
            return UIImage(cgImage: scaledImage, scale: scale, orientation: .up)
        }
        return nil
    }

}

extension CGSize {
    static func * (lhs: CGSize, rhs: CGFloat) -> CGSize {
        return CGSize(width: lhs.width * rhs, height: lhs.height * rhs)
    }
}

extension CGPoint {
    static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
        return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
    }
}

extension CGRect {
    static func * (lhs: CGRect, rhs: CGFloat) -> CGRect {
        return CGRect(origin: lhs.origin * rhs, size: lhs.size * rhs)
    }
}

话虽这么说,我们确实应该将并发请求限制在合理的范围内(一次 4-6 个),以便它们在先前的请求完成(或取消)之前不会尝试启动,以避免超时。典型的解决方案是用异步 Operation 子类包装请求,将它们添加到操作队列,并将 maxConcurrentOperationCount 限制为您选择的任何值。