AutorÃa | Ultima modificación | Ver Log |
//// ImageCache.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 Alamofireimport Foundation#if os(iOS) || os(tvOS) || os(watchOS)import UIKit#elseif os(macOS)import Cocoa#endif// MARK: ImageCache/// The `ImageCache` protocol defines a set of APIs for adding, removing and fetching images from a cache.public protocol ImageCache {/// Adds the image to the cache with the given identifier.func add(_ image: Image, withIdentifier identifier: String)/// Removes the image from the cache matching the given identifier.func removeImage(withIdentifier identifier: String) -> Bool/// Removes all images stored in the cache.@discardableResultfunc removeAllImages() -> Bool/// Returns the image in the cache associated with the given identifier.func image(withIdentifier identifier: String) -> Image?}/// The `ImageRequestCache` protocol extends the `ImageCache` protocol by adding methods for adding, removing and/// fetching images from a cache given an `URLRequest` and additional identifier.public protocol ImageRequestCache: ImageCache {/// Adds the image to the cache using an identifier created from the request and identifier.func add(_ image: Image, for request: URLRequest, withIdentifier identifier: String?)/// Removes the image from the cache using an identifier created from the request and identifier.func removeImage(for request: URLRequest, withIdentifier identifier: String?) -> Bool/// Returns the image from the cache associated with an identifier created from the request and identifier.func image(for request: URLRequest, withIdentifier identifier: String?) -> Image?}// MARK: -/// The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When/// the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously/// purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the/// internal access date of the image is updated.open class AutoPurgingImageCache: ImageRequestCache {class CachedImage {let image: Imagelet identifier: Stringlet totalBytes: UInt64var lastAccessDate: Dateinit(_ image: Image, identifier: String) {self.image = imageself.identifier = identifierlastAccessDate = Date()totalBytes = {#if os(iOS) || os(tvOS) || os(watchOS)let size = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)#elseif os(macOS)let size = CGSize(width: image.size.width, height: image.size.height)#endiflet bytesPerPixel: CGFloat = 4.0let bytesPerRow = size.width * bytesPerPixellet totalBytes = UInt64(bytesPerRow) * UInt64(size.height)return totalBytes}()}func accessImage() -> Image {lastAccessDate = Date()return image}}// MARK: Properties/// The current total memory usage in bytes of all images stored within the cache.open var memoryUsage: UInt64 {var memoryUsage: UInt64 = 0synchronizationQueue.sync(flags: [.barrier]) { memoryUsage = self.currentMemoryUsage }return memoryUsage}/// The total memory capacity of the cache in bytes.public let memoryCapacity: UInt64/// The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory/// capacity drops below this limit.public let preferredMemoryUsageAfterPurge: UInt64private let synchronizationQueue: DispatchQueueprivate var cachedImages: [String: CachedImage]private var currentMemoryUsage: UInt64// MARK: Initialization/// Initializes the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage/// after purge limit.////// Please note, the memory capacity must always be greater than or equal to the preferred memory usage after purge.////// - parameter memoryCapacity: The total memory capacity of the cache in bytes. `100 MB` by default./// - parameter preferredMemoryUsageAfterPurge: The preferred memory usage after purge in bytes. `60 MB` by default.////// - returns: The new `AutoPurgingImageCache` instance.public init(memoryCapacity: UInt64 = 100_000_000, preferredMemoryUsageAfterPurge: UInt64 = 60_000_000) {self.memoryCapacity = memoryCapacityself.preferredMemoryUsageAfterPurge = preferredMemoryUsageAfterPurgeprecondition(memoryCapacity >= preferredMemoryUsageAfterPurge,"The `memoryCapacity` must be greater than or equal to `preferredMemoryUsageAfterPurge`")cachedImages = [:]currentMemoryUsage = 0synchronizationQueue = {let name = String(format: "org.alamofire.autopurgingimagecache-%08x%08x", arc4random(), arc4random())return DispatchQueue(label: name, attributes: .concurrent)}()#if os(iOS) || os(tvOS)let notification = UIApplication.didReceiveMemoryWarningNotificationNotificationCenter.default.addObserver(self,selector: #selector(AutoPurgingImageCache.removeAllImages),name: notification,object: nil)#endif}deinit {NotificationCenter.default.removeObserver(self)}// MARK: Add Image to Cache/// Adds the image to the cache using an identifier created from the request and optional identifier.////// - parameter image: The image to add to the cache./// - parameter request: The request used to generate the image's unique identifier./// - parameter identifier: The additional identifier to append to the image's unique identifier.open func add(_ image: Image, for request: URLRequest, withIdentifier identifier: String? = nil) {let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)add(image, withIdentifier: requestIdentifier)}/// Adds the image to the cache with the given identifier.////// - parameter image: The image to add to the cache./// - parameter identifier: The identifier to use to uniquely identify the image.open func add(_ image: Image, withIdentifier identifier: String) {synchronizationQueue.async(flags: [.barrier]) {let cachedImage = CachedImage(image, identifier: identifier)if let previousCachedImage = self.cachedImages[identifier] {self.currentMemoryUsage -= previousCachedImage.totalBytes}self.cachedImages[identifier] = cachedImageself.currentMemoryUsage += cachedImage.totalBytes}synchronizationQueue.async(flags: [.barrier]) {if self.currentMemoryUsage > self.memoryCapacity {let bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurgevar sortedImages = self.cachedImages.map { $1 }sortedImages.sort {let date1 = $0.lastAccessDatelet date2 = $1.lastAccessDatereturn date1.timeIntervalSince(date2) < 0.0}var bytesPurged = UInt64(0)for cachedImage in sortedImages {self.cachedImages.removeValue(forKey: cachedImage.identifier)bytesPurged += cachedImage.totalBytesif bytesPurged >= bytesToPurge {break}}self.currentMemoryUsage -= bytesPurged}}}// MARK: Remove Image from Cache/// Removes the image from the cache using an identifier created from the request and optional identifier.////// - parameter request: The request used to generate the image's unique identifier./// - parameter identifier: The additional identifier to append to the image's unique identifier.////// - returns: `true` if the image was removed, `false` otherwise.@discardableResultopen func removeImage(for request: URLRequest, withIdentifier identifier: String?) -> Bool {let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)return removeImage(withIdentifier: requestIdentifier)}/// Removes all images from the cache created from the request.////// - parameter request: The request used to generate the image's unique identifier.////// - returns: `true` if any images were removed, `false` otherwise.@discardableResultopen func removeImages(matching request: URLRequest) -> Bool {let requestIdentifier = imageCacheKey(for: request, withIdentifier: nil)var removed = falsesynchronizationQueue.sync(flags: [.barrier]) {for key in self.cachedImages.keys where key.hasPrefix(requestIdentifier) {if let cachedImage = self.cachedImages.removeValue(forKey: key) {self.currentMemoryUsage -= cachedImage.totalBytesremoved = true}}}return removed}/// Removes the image from the cache matching the given identifier.////// - parameter identifier: The unique identifier for the image.////// - returns: `true` if the image was removed, `false` otherwise.@discardableResultopen func removeImage(withIdentifier identifier: String) -> Bool {var removed = falsesynchronizationQueue.sync(flags: [.barrier]) {if let cachedImage = self.cachedImages.removeValue(forKey: identifier) {self.currentMemoryUsage -= cachedImage.totalBytesremoved = true}}return removed}/// Removes all images stored in the cache.////// - returns: `true` if images were removed from the cache, `false` otherwise.@discardableResult @objcopen func removeAllImages() -> Bool {var removed = falsesynchronizationQueue.sync(flags: [.barrier]) {if !self.cachedImages.isEmpty {self.cachedImages.removeAll()self.currentMemoryUsage = 0removed = true}}return removed}// MARK: Fetch Image from Cache/// Returns the image from the cache associated with an identifier created from the request and optional identifier.////// - parameter request: The request used to generate the image's unique identifier./// - parameter identifier: The additional identifier to append to the image's unique identifier.////// - returns: The image if it is stored in the cache, `nil` otherwise.open func image(for request: URLRequest, withIdentifier identifier: String? = nil) -> Image? {let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)return image(withIdentifier: requestIdentifier)}/// Returns the image in the cache associated with the given identifier.////// - parameter identifier: The unique identifier for the image.////// - returns: The image if it is stored in the cache, `nil` otherwise.open func image(withIdentifier identifier: String) -> Image? {var image: Image?synchronizationQueue.sync(flags: [.barrier]) {if let cachedImage = self.cachedImages[identifier] {image = cachedImage.accessImage()}}return image}// MARK: Image Cache Keys/// Returns the unique image cache key for the specified request and additional identifier.////// - parameter request: The request./// - parameter identifier: The additional identifier.////// - returns: The unique image cache key.open func imageCacheKey(for request: URLRequest, withIdentifier identifier: String?) -> String {var key = request.url?.absoluteString ?? ""if let identifier = identifier {key += "-\(identifier)"}return key}}