且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

有没有一种方法可以使用URLSession.shared.dataTask并行请求多个不同的资源

更新时间:2022-06-22 22:24:37

您问:

是否有一种方法可以使用URLSession.shared.dataTask

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

By default, it does perform requests in parallel.

让我们退后一步:在您之前的问题中,您正在询问如何实现类似于Kingfisher的UIImageView扩展.在我的答案中,我提到使用objc_getAssociatedObjectobjc_setAssociatedObject来实现这一目标.但是在这里的问题中,您已经采用了关联的对象逻辑并将其放在您的DataRequest对象中.

Let’s step back for a second: In your prior question, you were asking how to implement a Kingfisher-like UIImageView extension. In my answer, I mentioned using objc_getAssociatedObject and objc_setAssociatedObject to achieve that. But in your question here, you’ve taken that associated object logic and put it in your DataRequest object.

您的思维过程中,将异步图像检索逻辑从UIImageView中拉出是一个好主意:您可能需要请求按钮的图像.您可能会使用通用的异步获取图像"例程,该例程与任何UIKit对象完全分离.因此,从扩展中抽象网络层代码是一个好主意.

Your thought process, to pull the asynchronous image retrieval logic out of the UIImageView is a good idea: You may want to request images for buttons. You might a general "fetch image asynchronously" routine, completely separate from any UIKit objects. So abstracting the network layer code out of the extension is an excellent idea.

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

But the whole idea behind asynchronous image retrieval UIImageView/UIButton extensions is that we want a UIKit control where not only can it perform asynchronous requests, but that if the cell with the control is reused, that it will cancel the prior asynchronous request (if any) before starting the next one. That way, if we scroll quickly down to images 80 through 99, the requests for cells 0 through 79 will be canceled, and the visible images won’t get backlogged behind all these old image requests.

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

But to achieve that, that means that the control needs some way to keep track of the prior request for that reused cell somehow. And because we can’t add stored properties in a UIImageView extension, that’s why we use the objc_getAssociatedObject and objc_setAssociatedObject pattern. But that has to be in the image view.

不幸的是,在上面的代码中,关联的对象位于DataRequest对象中.首先,正如我试图概述的那样,整个想法是图像视图必须跟踪对该控件的先前请求.在"DataRequest"对象中放置跟踪先前的请求"会破坏该目的.其次,值得注意的是,您不需要自己的类型(如DataRequest)中的关联对象.您只会拥有一个存储的财产.扩展UIImageView之类的另一种类型时,只需要遍历此关联的对象无聊即可.

Unfortunately, in your code above, the associated object is in your DataRequest object. First, as I’ve tried to outline, the whole idea is that the image view must keep track of the prior request for that control. Putting this "keep track of the prior request" inside the DataRequest object defeats that purpose. Second, it’s worth noting that you don’t need associated objects in your own types, like DataRequest. You’d just have a stored property. You only need to go through this associated object silliness when extending another type, such as UIImageView.

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

Below, is a quick example that I whipped together showing a UIImageView extension for asynchronous image retrieval. Note, this doesn’t have the abstraction of the network code out of the extension, but do note that the associated object logic to keep track of the prior request must remain with the extension.

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)
            }
        }
    }
}

这是我调整大小的例程:

And this is my resizing routine:

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: $0, 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约束为您选择的任何值.


That having been said, we really should constrain our concurrent requests to something reasonable (4-6 at a time) so that they don’t try to start until the prior requests are done (or are canceled) to avoid timeouts. The typical solution is wrapping the requests with asynchronous Operation subclasses, add them to an operation queue, and constrain the maxConcurrentOperationCount to whatever value you choose.