ImageCache.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. // ImageCache.swift
  2. //
  3. // Copyright (c) 2015-2016 Alamofire Software Foundation (http://alamofire.org/)
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. import Alamofire
  23. import Foundation
  24. #if os(iOS) || os(tvOS) || os(watchOS)
  25. import UIKit
  26. #elseif os(OSX)
  27. import Cocoa
  28. #endif
  29. // MARK: ImageCache
  30. /// The `ImageCache` protocol defines a set of APIs for adding, removing and fetching images from a cache.
  31. public protocol ImageCache {
  32. /// Adds the image to the cache with the given identifier.
  33. func addImage(image: Image, withIdentifier identifier: String)
  34. /// Removes the image from the cache matching the given identifier.
  35. func removeImageWithIdentifier(identifier: String) -> Bool
  36. /// Removes all images stored in the cache.
  37. func removeAllImages() -> Bool
  38. /// Returns the image in the cache associated with the given identifier.
  39. func imageWithIdentifier(identifier: String) -> Image?
  40. }
  41. /// The `ImageRequestCache` protocol extends the `ImageCache` protocol by adding methods for adding, removing and
  42. /// fetching images from a cache given an `NSURLRequest` and additional identifier.
  43. public protocol ImageRequestCache: ImageCache {
  44. /// Adds the image to the cache using an identifier created from the request and additional identifier.
  45. func addImage(image: Image, forRequest request: NSURLRequest, withAdditionalIdentifier identifier: String?)
  46. /// Removes the image from the cache using an identifier created from the request and additional identifier.
  47. func removeImageForRequest(request: NSURLRequest, withAdditionalIdentifier identifier: String?) -> Bool
  48. /// Returns the image from the cache associated with an identifier created from the request and additional identifier.
  49. func imageForRequest(request: NSURLRequest, withAdditionalIdentifier identifier: String?) -> Image?
  50. }
  51. // MARK: -
  52. /// The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When
  53. /// the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously
  54. /// purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the
  55. /// internal access date of the image is updated.
  56. public class AutoPurgingImageCache: ImageRequestCache {
  57. private class CachedImage {
  58. let image: Image
  59. let identifier: String
  60. let totalBytes: UInt64
  61. var lastAccessDate: NSDate
  62. init(_ image: Image, identifier: String) {
  63. self.image = image
  64. self.identifier = identifier
  65. self.lastAccessDate = NSDate()
  66. self.totalBytes = {
  67. #if os(iOS) || os(tvOS) || os(watchOS)
  68. let size = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
  69. #elseif os(OSX)
  70. let size = CGSize(width: image.size.width, height: image.size.height)
  71. #endif
  72. let bytesPerPixel: CGFloat = 4.0
  73. let bytesPerRow = size.width * bytesPerPixel
  74. let totalBytes = UInt64(bytesPerRow) * UInt64(size.height)
  75. return totalBytes
  76. }()
  77. }
  78. func accessImage() -> Image {
  79. lastAccessDate = NSDate()
  80. return image
  81. }
  82. }
  83. // MARK: Properties
  84. /// The current total memory usage in bytes of all images stored within the cache.
  85. public var memoryUsage: UInt64 {
  86. var memoryUsage: UInt64 = 0
  87. dispatch_sync(synchronizationQueue) { memoryUsage = self.currentMemoryUsage }
  88. return memoryUsage
  89. }
  90. /// The total memory capacity of the cache in bytes.
  91. public let memoryCapacity: UInt64
  92. /// The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory
  93. /// capacity drops below this limit.
  94. public let preferredMemoryUsageAfterPurge: UInt64
  95. private let synchronizationQueue: dispatch_queue_t
  96. private var cachedImages: [String: CachedImage]
  97. private var currentMemoryUsage: UInt64
  98. // MARK: Initialization
  99. /**
  100. Initialies the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage
  101. after purge limit.
  102. Please note, the memory capacity must always be greater than or equal to the preferred memory usage after purge.
  103. - parameter memoryCapacity: The total memory capacity of the cache in bytes. `100 MB` by default.
  104. - parameter preferredMemoryUsageAfterPurge: The preferred memory usage after purge in bytes. `60 MB` by default.
  105. - returns: The new `AutoPurgingImageCache` instance.
  106. */
  107. public init(memoryCapacity: UInt64 = 100_000_000, preferredMemoryUsageAfterPurge: UInt64 = 60_000_000) {
  108. self.memoryCapacity = memoryCapacity
  109. self.preferredMemoryUsageAfterPurge = preferredMemoryUsageAfterPurge
  110. precondition(
  111. memoryCapacity >= preferredMemoryUsageAfterPurge,
  112. "The `memoryCapacity` must be greater than or equal to `preferredMemoryUsageAfterPurge`"
  113. )
  114. self.cachedImages = [:]
  115. self.currentMemoryUsage = 0
  116. self.synchronizationQueue = {
  117. let name = String(format: "com.alamofire.autopurgingimagecache-%08%08", arc4random(), arc4random())
  118. return dispatch_queue_create(name, DISPATCH_QUEUE_CONCURRENT)
  119. }()
  120. #if os(iOS)
  121. NSNotificationCenter.defaultCenter().addObserver(
  122. self,
  123. selector: "removeAllImages",
  124. name: UIApplicationDidReceiveMemoryWarningNotification,
  125. object: nil
  126. )
  127. #endif
  128. }
  129. deinit {
  130. NSNotificationCenter.defaultCenter().removeObserver(self)
  131. }
  132. // MARK: Add Image to Cache
  133. /**
  134. Adds the image to the cache using an identifier created from the request and optional identifier.
  135. - parameter image: The image to add to the cache.
  136. - parameter request: The request used to generate the image's unique identifier.
  137. - parameter identifier: The additional identifier to append to the image's unique identifier.
  138. */
  139. public func addImage(image: Image, forRequest request: NSURLRequest, withAdditionalIdentifier identifier: String? = nil) {
  140. let requestIdentifier = imageCacheKeyFromURLRequest(request, withAdditionalIdentifier: identifier)
  141. addImage(image, withIdentifier: requestIdentifier)
  142. }
  143. /**
  144. Adds the image to the cache with the given identifier.
  145. - parameter image: The image to add to the cache.
  146. - parameter identifier: The identifier to use to uniquely identify the image.
  147. */
  148. public func addImage(image: Image, withIdentifier identifier: String) {
  149. dispatch_barrier_async(synchronizationQueue) {
  150. let cachedImage = CachedImage(image, identifier: identifier)
  151. if let previousCachedImage = self.cachedImages[identifier] {
  152. self.currentMemoryUsage -= previousCachedImage.totalBytes
  153. }
  154. self.cachedImages[identifier] = cachedImage
  155. self.currentMemoryUsage += cachedImage.totalBytes
  156. }
  157. dispatch_barrier_async(synchronizationQueue) {
  158. if self.currentMemoryUsage > self.memoryCapacity {
  159. let bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge
  160. var sortedImages = [CachedImage](self.cachedImages.values)
  161. sortedImages.sortInPlace {
  162. let date1 = $0.lastAccessDate
  163. let date2 = $1.lastAccessDate
  164. return date1.timeIntervalSinceDate(date2) < 0.0
  165. }
  166. var bytesPurged = UInt64(0)
  167. for cachedImage in sortedImages {
  168. self.cachedImages.removeValueForKey(cachedImage.identifier)
  169. bytesPurged += cachedImage.totalBytes
  170. if bytesPurged >= bytesToPurge {
  171. break
  172. }
  173. }
  174. self.currentMemoryUsage -= bytesPurged
  175. }
  176. }
  177. }
  178. // MARK: Remove Image from Cache
  179. /**
  180. Removes the image from the cache using an identifier created from the request and optional identifier.
  181. - parameter request: The request used to generate the image's unique identifier.
  182. - parameter identifier: The additional identifier to append to the image's unique identifier.
  183. - returns: `true` if the image was removed, `false` otherwise.
  184. */
  185. public func removeImageForRequest(request: NSURLRequest, withAdditionalIdentifier identifier: String?) -> Bool {
  186. let requestIdentifier = imageCacheKeyFromURLRequest(request, withAdditionalIdentifier: identifier)
  187. return removeImageWithIdentifier(requestIdentifier)
  188. }
  189. /**
  190. Removes the image from the cache matching the given identifier.
  191. - parameter identifier: The unique identifier for the image.
  192. - returns: `true` if the image was removed, `false` otherwise.
  193. */
  194. public func removeImageWithIdentifier(identifier: String) -> Bool {
  195. var removed = false
  196. dispatch_barrier_async(synchronizationQueue) {
  197. if let cachedImage = self.cachedImages.removeValueForKey(identifier) {
  198. self.currentMemoryUsage -= cachedImage.totalBytes
  199. removed = true
  200. }
  201. }
  202. return removed
  203. }
  204. /**
  205. Removes all images stored in the cache.
  206. - returns: `true` if images were removed from the cache, `false` otherwise.
  207. */
  208. @objc public func removeAllImages() -> Bool {
  209. var removed = false
  210. dispatch_sync(synchronizationQueue) {
  211. if !self.cachedImages.isEmpty {
  212. self.cachedImages.removeAll()
  213. self.currentMemoryUsage = 0
  214. removed = true
  215. }
  216. }
  217. return removed
  218. }
  219. // MARK: Fetch Image from Cache
  220. /**
  221. Returns the image from the cache associated with an identifier created from the request and optional identifier.
  222. - parameter request: The request used to generate the image's unique identifier.
  223. - parameter identifier: The additional identifier to append to the image's unique identifier.
  224. - returns: The image if it is stored in the cache, `nil` otherwise.
  225. */
  226. public func imageForRequest(request: NSURLRequest, withAdditionalIdentifier identifier: String? = nil) -> Image? {
  227. let requestIdentifier = imageCacheKeyFromURLRequest(request, withAdditionalIdentifier: identifier)
  228. return imageWithIdentifier(requestIdentifier)
  229. }
  230. /**
  231. Returns the image in the cache associated with the given identifier.
  232. - parameter identifier: The unique identifier for the image.
  233. - returns: The image if it is stored in the cache, `nil` otherwise.
  234. */
  235. public func imageWithIdentifier(identifier: String) -> Image? {
  236. var image: Image?
  237. dispatch_sync(synchronizationQueue) {
  238. if let cachedImage = self.cachedImages[identifier] {
  239. image = cachedImage.accessImage()
  240. }
  241. }
  242. return image
  243. }
  244. // MARK: Private - Helper Methods
  245. private func imageCacheKeyFromURLRequest(
  246. request: NSURLRequest,
  247. withAdditionalIdentifier identifier: String?)
  248. -> String
  249. {
  250. var key = request.URLString
  251. if let identifier = identifier {
  252. key += "-\(identifier)"
  253. }
  254. return key
  255. }
  256. }