Няма описание

ImageDownloader.swift 24KB

    // // ImageDownloader.swift // // Copyright (c) 2015-2017 Alamofire Software Foundation (http://alamofire.org/) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import Alamofire import Foundation #if os(iOS) || os(tvOS) || os(watchOS) import UIKit #elseif os(macOS) import Cocoa #endif /// The `RequestReceipt` is an object vended by the `ImageDownloader` when starting a download request. It can be used /// to cancel active requests running on the `ImageDownloader` session. As a general rule, image download requests /// should be cancelled using the `RequestReceipt` instead of calling `cancel` directly on the `request` itself. The /// `ImageDownloader` is optimized to handle duplicate request scenarios as well as pending versus active downloads. open class RequestReceipt { /// The download request created by the `ImageDownloader`. open let request: Request /// The unique identifier for the image filters and completion handlers when duplicate requests are made. open let receiptID: String init(request: Request, receiptID: String) { self.request = request self.receiptID = receiptID } } // MARK: - /// The `ImageDownloader` class is responsible for downloading images in parallel on a prioritized queue. Incoming /// downloads are added to the front or back of the queue depending on the download prioritization. Each downloaded /// image is cached in the underlying `NSURLCache` as well as the in-memory image cache that supports image filters. /// By default, any download request with a cached image equivalent in the image cache will automatically be served the /// cached image representation. Additional advanced features include supporting multiple image filters and completion /// handlers for a single request. open class ImageDownloader { /// The completion handler closure used when an image download completes. public typealias CompletionHandler = (DataResponse<Image>) -> Void /// The progress handler closure called periodically during an image download. public typealias ProgressHandler = DataRequest.ProgressHandler // MARK: Helper Types /// Defines the order prioritization of incoming download requests being inserted into the queue. /// /// - fifo: All incoming downloads are added to the back of the queue. /// - lifo: All incoming downloads are added to the front of the queue. public enum DownloadPrioritization { case fifo, lifo } class ResponseHandler { let urlID: String let handlerID: String let request: DataRequest var operations: [(receiptID: String, filter: ImageFilter?, completion: CompletionHandler?)] init( request: DataRequest, handlerID: String, receiptID: String, filter: ImageFilter?, completion: CompletionHandler?) { self.request = request self.urlID = ImageDownloader.urlIdentifier(for: request.request!) self.handlerID = handlerID self.operations = [(receiptID: receiptID, filter: filter, completion: completion)] } } // MARK: Properties /// The image cache used to store all downloaded images in. open let imageCache: ImageRequestCache? /// The credential used for authenticating each download request. open private(set) var credential: URLCredential? /// Response serializer used to convert the image data to UIImage. public var imageResponseSerializer = DataRequest.imageResponseSerializer() /// The underlying Alamofire `Manager` instance used to handle all download requests. open let sessionManager: SessionManager let downloadPrioritization: DownloadPrioritization let maximumActiveDownloads: Int var activeRequestCount = 0 var queuedRequests: [Request] = [] var responseHandlers: [String: ResponseHandler] = [:] private let synchronizationQueue: DispatchQueue = { let name = String(format: "org.alamofire.imagedownloader.synchronizationqueue-%08x%08x", arc4random(), arc4random()) return DispatchQueue(label: name) }() private let responseQueue: DispatchQueue = { let name = String(format: "org.alamofire.imagedownloader.responsequeue-%08x%08x", arc4random(), arc4random()) return DispatchQueue(label: name, attributes: .concurrent) }() // MARK: Initialization /// The default instance of `ImageDownloader` initialized with default values. open static let `default` = ImageDownloader() /// Creates a default `URLSessionConfiguration` with common usage parameter values. /// /// - returns: The default `URLSessionConfiguration` instance. open class func defaultURLSessionConfiguration() -> URLSessionConfiguration { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders configuration.httpShouldSetCookies = true configuration.httpShouldUsePipelining = false configuration.requestCachePolicy = .useProtocolCachePolicy configuration.allowsCellularAccess = true configuration.timeoutIntervalForRequest = 60 configuration.urlCache = ImageDownloader.defaultURLCache() return configuration } /// Creates a default `URLCache` with common usage parameter values. /// /// - returns: The default `URLCache` instance. open class func defaultURLCache() -> URLCache { return URLCache( memoryCapacity: 20 * 1024 * 1024, // 20 MB diskCapacity: 150 * 1024 * 1024, // 150 MB diskPath: "org.alamofire.imagedownloader" ) } /// Initializes the `ImageDownloader` instance with the given configuration, download prioritization, maximum active /// download count and image cache. /// /// - parameter configuration: The `URLSessionConfiguration` to use to create the underlying Alamofire /// `SessionManager` instance. /// - parameter downloadPrioritization: The download prioritization of the download queue. `.fifo` by default. /// - parameter maximumActiveDownloads: The maximum number of active downloads allowed at any given time. /// - parameter imageCache: The image cache used to store all downloaded images in. /// /// - returns: The new `ImageDownloader` instance. public init( configuration: URLSessionConfiguration = ImageDownloader.defaultURLSessionConfiguration(), downloadPrioritization: DownloadPrioritization = .fifo, maximumActiveDownloads: Int = 4, imageCache: ImageRequestCache? = AutoPurgingImageCache()) { self.sessionManager = SessionManager(configuration: configuration) self.sessionManager.startRequestsImmediately = false self.downloadPrioritization = downloadPrioritization self.maximumActiveDownloads = maximumActiveDownloads self.imageCache = imageCache } /// Initializes the `ImageDownloader` instance with the given session manager, download prioritization, maximum /// active download count and image cache. /// /// - parameter sessionManager: The Alamofire `SessionManager` instance to handle all download requests. /// - parameter downloadPrioritization: The download prioritization of the download queue. `.fifo` by default. /// - parameter maximumActiveDownloads: The maximum number of active downloads allowed at any given time. /// - parameter imageCache: The image cache used to store all downloaded images in. /// /// - returns: The new `ImageDownloader` instance. public init( sessionManager: SessionManager, downloadPrioritization: DownloadPrioritization = .fifo, maximumActiveDownloads: Int = 4, imageCache: ImageRequestCache? = AutoPurgingImageCache()) { self.sessionManager = sessionManager self.sessionManager.startRequestsImmediately = false self.downloadPrioritization = downloadPrioritization self.maximumActiveDownloads = maximumActiveDownloads self.imageCache = imageCache } // MARK: Authentication /// Associates an HTTP Basic Auth credential with all future download requests. /// /// - parameter user: The user. /// - parameter password: The password. /// - parameter persistence: The URL credential persistence. `.forSession` by default. open func addAuthentication( user: String, password: String, persistence: URLCredential.Persistence = .forSession) { let credential = URLCredential(user: user, password: password, persistence: persistence) addAuthentication(usingCredential: credential) } /// Associates the specified credential with all future download requests. /// /// - parameter credential: The credential. open func addAuthentication(usingCredential credential: URLCredential) { synchronizationQueue.sync { self.credential = credential } } // MARK: Download /// Creates a download request using the internal Alamofire `SessionManager` instance for the specified URL request. /// /// If the same download request is already in the queue or currently being downloaded, the filter and completion /// handler are appended to the already existing request. Once the request completes, all filters and completion /// handlers attached to the request are executed in the order they were added. Additionally, any filters attached /// to the request with the same identifiers are only executed once. The resulting image is then passed into each /// completion handler paired with the filter. /// /// You should not attempt to directly cancel the `request` inside the request receipt since other callers may be /// relying on the completion of that request. Instead, you should call `cancelRequestForRequestReceipt` with the /// returned request receipt to allow the `ImageDownloader` to optimize the cancellation on behalf of all active /// callers. /// /// - parameter urlRequest: The URL request. /// - parameter receiptID: The `identifier` for the `RequestReceipt` returned. Defaults to a new, randomly /// generated UUID. /// - parameter filter: The image filter to apply to the image after the download is complete. Defaults /// to `nil`. /// - parameter progress: The closure to be executed periodically during the lifecycle of the request. /// Defaults to `nil`. /// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue. /// - parameter completion: The closure called when the download request is complete. Defaults to `nil`. /// /// - returns: The request receipt for the download request if available. `nil` if the image is stored in the image /// cache and the URL request cache policy allows the cache to be used. @discardableResult open func download( _ urlRequest: URLRequestConvertible, receiptID: String = UUID().uuidString, filter: ImageFilter? = nil, progress: ProgressHandler? = nil, progressQueue: DispatchQueue = DispatchQueue.main, completion: CompletionHandler?) -> RequestReceipt? { var request: DataRequest! synchronizationQueue.sync { // 1) Append the filter and completion handler to a pre-existing request if it already exists let urlID = ImageDownloader.urlIdentifier(for: urlRequest) if let responseHandler = self.responseHandlers[urlID] { responseHandler.operations.append((receiptID: receiptID, filter: filter, completion: completion)) request = responseHandler.request return } // 2) Attempt to load the image from the image cache if the cache policy allows it if let request = urlRequest.urlRequest { switch request.cachePolicy { case .useProtocolCachePolicy, .returnCacheDataElseLoad, .returnCacheDataDontLoad: if let image = self.imageCache?.image(for: request, withIdentifier: filter?.identifier) { DispatchQueue.main.async { let response = DataResponse<Image>( request: urlRequest.urlRequest, response: nil, data: nil, result: .success(image) ) completion?(response) } return } default: break } } // 3) Create the request and set up authentication, validation and response serialization request = self.sessionManager.request(urlRequest) if let credential = self.credential { request.authenticate(usingCredential: credential) } request.validate() if let progress = progress { request.downloadProgress(queue: progressQueue, closure: progress) } // Generate a unique handler id to check whether the active request has changed while downloading let handlerID = UUID().uuidString request.response( queue: self.responseQueue, responseSerializer: imageResponseSerializer, completionHandler: { [weak self] response in guard let strongSelf = self, let request = response.request else { return } defer { strongSelf.safelyDecrementActiveRequestCount() strongSelf.safelyStartNextRequestIfNecessary() } // Early out if the request has changed out from under us let handler = strongSelf.safelyFetchResponseHandler(withURLIdentifier: urlID) guard handler?.handlerID == handlerID else { return } guard let responseHandler = strongSelf.safelyRemoveResponseHandler(withURLIdentifier: urlID) else { return } switch response.result { case .success(let image): var filteredImages: [String: Image] = [:] for (_, filter, completion) in responseHandler.operations { var filteredImage: Image if let filter = filter { if let alreadyFilteredImage = filteredImages[filter.identifier] { filteredImage = alreadyFilteredImage } else { filteredImage = filter.filter(image) filteredImages[filter.identifier] = filteredImage } } else { filteredImage = image } strongSelf.imageCache?.add(filteredImage, for: request, withIdentifier: filter?.identifier) DispatchQueue.main.async { let response = DataResponse<Image>( request: response.request, response: response.response, data: response.data, result: .success(filteredImage), timeline: response.timeline ) completion?(response) } } case .failure: for (_, _, completion) in responseHandler.operations { DispatchQueue.main.async { completion?(response) } } } } ) // 4) Store the response handler for use when the request completes let responseHandler = ResponseHandler( request: request, handlerID: handlerID, receiptID: receiptID, filter: filter, completion: completion ) self.responseHandlers[urlID] = responseHandler // 5) Either start the request or enqueue it depending on the current active request count if self.isActiveRequestCountBelowMaximumLimit() { self.start(request) } else { self.enqueue(request) } } if let request = request { return RequestReceipt(request: request, receiptID: receiptID) } return nil } /// Creates a download request using the internal Alamofire `SessionManager` instance for each specified URL request. /// /// For each request, if the same download request is already in the queue or currently being downloaded, the /// filter and completion handler are appended to the already existing request. Once the request completes, all /// filters and completion handlers attached to the request are executed in the order they were added. /// Additionally, any filters attached to the request with the same identifiers are only executed once. The /// resulting image is then passed into each completion handler paired with the filter. /// /// You should not attempt to directly cancel any of the `request`s inside the request receipts array since other /// callers may be relying on the completion of that request. Instead, you should call /// `cancelRequestForRequestReceipt` with the returned request receipt to allow the `ImageDownloader` to optimize /// the cancellation on behalf of all active callers. /// /// - parameter urlRequests: The URL requests. /// - parameter filter The image filter to apply to the image after each download is complete. /// - parameter progress: The closure to be executed periodically during the lifecycle of the request. Defaults /// to `nil`. /// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue. /// - parameter completion: The closure called when each download request is complete. /// /// - returns: The request receipts for the download requests if available. If an image is stored in the image /// cache and the URL request cache policy allows the cache to be used, a receipt will not be returned /// for that request. @discardableResult open func download( _ urlRequests: [URLRequestConvertible], filter: ImageFilter? = nil, progress: ProgressHandler? = nil, progressQueue: DispatchQueue = DispatchQueue.main, completion: CompletionHandler? = nil) -> [RequestReceipt] { return urlRequests.flatMap { download($0, filter: filter, progress: progress, progressQueue: progressQueue, completion: completion) } } /// Cancels the request in the receipt by removing the response handler and cancelling the request if necessary. /// /// If the request is pending in the queue, it will be cancelled if no other response handlers are registered with /// the request. If the request is currently executing or is already completed, the response handler is removed and /// will not be called. /// /// - parameter requestReceipt: The request receipt to cancel. open func cancelRequest(with requestReceipt: RequestReceipt) { synchronizationQueue.sync { let urlID = ImageDownloader.urlIdentifier(for: requestReceipt.request.request!) guard let responseHandler = self.responseHandlers[urlID] else { return } if let index = responseHandler.operations.index(where: { $0.receiptID == requestReceipt.receiptID }) { let operation = responseHandler.operations.remove(at: index) let response: DataResponse<Image> = { let urlRequest = requestReceipt.request.request let error = AFIError.requestCancelled return DataResponse(request: urlRequest, response: nil, data: nil, result: .failure(error)) }() DispatchQueue.main.async { operation.completion?(response) } } if responseHandler.operations.isEmpty && requestReceipt.request.task?.state == .suspended { requestReceipt.request.cancel() self.responseHandlers.removeValue(forKey: urlID) } } } // MARK: Internal - Thread-Safe Request Methods func safelyFetchResponseHandler(withURLIdentifier urlIdentifier: String) -> ResponseHandler? { var responseHandler: ResponseHandler? synchronizationQueue.sync { responseHandler = self.responseHandlers[urlIdentifier] } return responseHandler } func safelyRemoveResponseHandler(withURLIdentifier identifier: String) -> ResponseHandler? { var responseHandler: ResponseHandler? synchronizationQueue.sync { responseHandler = self.responseHandlers.removeValue(forKey: identifier) } return responseHandler } func safelyStartNextRequestIfNecessary() { synchronizationQueue.sync { guard self.isActiveRequestCountBelowMaximumLimit() else { return } while !self.queuedRequests.isEmpty { if let request = self.dequeue(), request.task?.state == .suspended { self.start(request) break } } } } func safelyDecrementActiveRequestCount() { self.synchronizationQueue.sync { if self.activeRequestCount > 0 { self.activeRequestCount -= 1 } } } // MARK: Internal - Non Thread-Safe Request Methods func start(_ request: Request) { request.resume() activeRequestCount += 1 } func enqueue(_ request: Request) { switch downloadPrioritization { case .fifo: queuedRequests.append(request) case .lifo: queuedRequests.insert(request, at: 0) } } @discardableResult func dequeue() -> Request? { var request: Request? if !queuedRequests.isEmpty { request = queuedRequests.removeFirst() } return request } func isActiveRequestCountBelowMaximumLimit() -> Bool { return activeRequestCount < maximumActiveDownloads } static func urlIdentifier(for urlRequest: URLRequestConvertible) -> String { return urlRequest.urlRequest?.url?.absoluteString ?? "" } }