You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
weibo/Pods/AlamofireImage/Source/ImageDownloader.swift

579 lines
27 KiB

4 years ago
//
// ImageDownloader.swift
//
// Copyright (c) 2015 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
/// Alias for `DataResponse<T, AFIError>`.
public typealias AFIDataResponse<T> = DataResponse<T, AFIError>
/// Alias for `Result<T, AFIError>`.
public typealias AFIResult<T> = Result<T, AFIError>
/// 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`.
public let request: DataRequest
/// The unique identifier for the image filters and completion handlers when duplicate requests are made.
public let receiptID: String
init(request: DataRequest, 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 = (AFIDataResponse<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
urlID = ImageDownloader.urlIdentifier(for: request.convertible)
self.handlerID = handlerID
operations = [(receiptID: receiptID, filter: filter, completion: completion)]
}
}
// MARK: Properties
/// The image cache used to store all downloaded images in.
public 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 = ImageResponseSerializer()
/// The underlying Alamofire `Session` instance used to handle all download requests.
public let session: Session
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.
public 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.headers = .default
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 {
let memoryCapacity = 20 * 1024 * 1024
let diskCapacity = 150 * 1024 * 1024
let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
let imageDownloaderPath = "org.alamofire.imagedownloader"
#if targetEnvironment(macCatalyst)
return URLCache(memoryCapacity: memoryCapacity,
diskCapacity: diskCapacity,
directory: cacheDirectory?.appendingPathComponent(imageDownloaderPath))
#else
#if os(macOS)
return URLCache(memoryCapacity: memoryCapacity,
diskCapacity: diskCapacity,
diskPath: cacheDirectory?.appendingPathComponent(imageDownloaderPath).absoluteString)
#else
return URLCache(memoryCapacity: memoryCapacity,
diskCapacity: diskCapacity,
diskPath: imageDownloaderPath)
#endif
#endif
}
/// 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()) {
session = Session(configuration: configuration, 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 session: The Alamofire `Session` 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(session: Session,
downloadPrioritization: DownloadPrioritization = .fifo,
maximumActiveDownloads: Int = 4,
imageCache: ImageRequestCache? = AutoPurgingImageCache()) {
precondition(!session.startRequestsImmediately, "Session must set `startRequestsImmediately` to `false`.")
self.session = session
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 cacheKey: An optional key used to identify the image in the cache. Defaults to `nil`.
/// - parameter receiptID: The `identifier` for the `RequestReceipt` returned. Defaults to a new, randomly
/// generated UUID.
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`. Defaults
/// to `nil` which will fall back to the instance `imageResponseSerializer`.
/// - 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,
cacheKey: String? = nil,
receiptID: String = UUID().uuidString,
serializer: ImageResponseSerializer? = nil,
filter: ImageFilter? = nil,
progress: ProgressHandler? = nil,
progressQueue: DispatchQueue = DispatchQueue.main,
completion: CompletionHandler? = nil)
-> RequestReceipt? {
var queuedRequest: 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))
queuedRequest = responseHandler.request
return
}
// 2) Attempt to load the image from the image cache if the cache policy allows it
if let nonNilURLRequest = urlRequest.urlRequest {
switch nonNilURLRequest.cachePolicy {
case .useProtocolCachePolicy, .returnCacheDataElseLoad, .returnCacheDataDontLoad:
let cachedImage: Image?
if let cacheKey = cacheKey {
cachedImage = self.imageCache?.image(withIdentifier: cacheKey)
} else {
cachedImage = self.imageCache?.image(for: nonNilURLRequest, withIdentifier: filter?.identifier)
}
if let image = cachedImage {
DispatchQueue.main.async {
let response = AFIDataResponse<Image>(request: urlRequest.urlRequest,
response: nil,
data: nil,
metrics: nil,
serializationDuration: 0.0,
result: .success(image))
completion?(response)
}
return
}
default:
break
}
}
// 3) Create the request and set up authentication, validation and response serialization
let request = self.session.request(urlRequest)
queuedRequest = request
if let credential = self.credential {
request.authenticate(with: 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: serializer ?? imageResponseSerializer,
completionHandler: { response in
defer {
self.safelyDecrementActiveRequestCount()
self.safelyStartNextRequestIfNecessary()
}
// Early out if the request has changed out from under us
guard
let handler = self.safelyFetchResponseHandler(withURLIdentifier: urlID),
handler.handlerID == handlerID,
let responseHandler = self.safelyRemoveResponseHandler(withURLIdentifier: urlID)
else {
return
}
switch response.result {
case let .success(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
}
if let cacheKey = cacheKey {
self.imageCache?.add(filteredImage, withIdentifier: cacheKey)
} else if let request = response.request {
self.imageCache?.add(filteredImage, for: request, withIdentifier: filter?.identifier)
}
DispatchQueue.main.async {
let response = AFIDataResponse<Image>(request: response.request,
response: response.response,
data: response.data,
metrics: response.metrics,
serializationDuration: response.serializationDuration,
result: .success(filteredImage))
completion?(response)
}
}
case .failure:
for (_, _, completion) in responseHandler.operations {
DispatchQueue.main.async { completion?(response.mapError { AFIError.alamofireError($0) }) }
}
}
})
// 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 = queuedRequest {
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] {
urlRequests.compactMap {
download($0, filter: filter, progress: progress, progressQueue: progressQueue, completion: completion)
}
}
/// Cancels the request contained inside the receipt calls the completion handler with a request cancelled error.
///
/// - Parameter requestReceipt: The request receipt to cancel.
open func cancelRequest(with requestReceipt: RequestReceipt) {
synchronizationQueue.sync {
let urlID = ImageDownloader.urlIdentifier(for: requestReceipt.request.convertible)
guard let responseHandler = self.responseHandlers[urlID] else { return }
let index = responseHandler.operations.firstIndex { $0.receiptID == requestReceipt.receiptID }
if let index = index {
let operation = responseHandler.operations.remove(at: index)
let response: AFIDataResponse<Image> = {
let urlRequest = requestReceipt.request.request
let error = AFIError.requestCancelled
return DataResponse(request: urlRequest,
response: nil,
data: nil,
metrics: nil,
serializationDuration: 0.0,
result: .failure(error))
}()
DispatchQueue.main.async { operation.completion?(response) }
}
if responseHandler.operations.isEmpty {
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 }
guard let request = self.dequeue() else { return }
self.start(request)
}
}
func safelyDecrementActiveRequestCount() {
synchronizationQueue.sync {
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 {
activeRequestCount < maximumActiveDownloads
}
static func urlIdentifier(for urlRequest: URLRequestConvertible) -> String {
var urlID: String?
do {
urlID = try urlRequest.asURLRequest().url?.absoluteString
} catch {
// No-op
}
return urlID ?? ""
}
}