ImageCache.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. //
  2. // ImageCache.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/6.
  6. //
  7. // Copyright (c) 2015 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import Foundation
  27. private let defaultCacheName = "default"
  28. private let cacheReverseDNS = "com.onevcat.Kingfisher.ImageCache."
  29. private let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue"
  30. private let processQueueName = "com.onevcat.Kingfisher.ImageCache.processQueue"
  31. private let defaultCacheInstance = ImageCache(name: defaultCacheName)
  32. private let defaultMaxCachePeriodInSecond: NSTimeInterval = 60 * 60 * 24 * 7 //Cache exists for 1 week
  33. public typealias RetrieveImageDiskTask = dispatch_block_t
  34. /**
  35. Cache type of a cached image.
  36. - Memory: The image is cached in memory.
  37. - Disk: The image is cached in disk.
  38. */
  39. public enum CacheType {
  40. case Memory, Disk
  41. }
  42. public class ImageCache {
  43. //Memory
  44. private let memoryCache = NSCache()
  45. /// The largest cache cost of memory cache. The total cost is pixel count of all cached images in memory.
  46. public var maxMemoryCost: UInt = 0 {
  47. didSet {
  48. self.memoryCache.totalCostLimit = Int(maxMemoryCost)
  49. }
  50. }
  51. //Disk
  52. private let ioQueue = dispatch_queue_create(ioQueueName, DISPATCH_QUEUE_SERIAL)
  53. private let diskCachePath: String
  54. private var fileManager: NSFileManager!
  55. /// The longest time duration of the cache being stored in disk. Default is 1 week.
  56. public var maxCachePeriodInSecond = defaultMaxCachePeriodInSecond
  57. /// The largest disk size can be taken for the cache. It is the total allocated size of the file in bytes. Default is 0, which means no limit.
  58. public var maxDiskCacheSize: UInt = 0
  59. private let processQueue = dispatch_queue_create(processQueueName, DISPATCH_QUEUE_CONCURRENT)
  60. /// The default cache.
  61. public class var defaultCache: ImageCache {
  62. return defaultCacheInstance
  63. }
  64. /**
  65. Init method. Passing a name for the cache. It represents a cache folder in the memory and disk.
  66. :param: name Name of the cache.
  67. :returns: The cache object.
  68. */
  69. public init(name: String) {
  70. let cacheName = cacheReverseDNS + name
  71. memoryCache.name = cacheName
  72. let paths = NSSearchPathForDirectoriesInDomains(.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true)
  73. diskCachePath = paths.first!.stringByAppendingPathComponent(cacheName)
  74. dispatch_sync(ioQueue, { () -> Void in
  75. self.fileManager = NSFileManager()
  76. })
  77. NSNotificationCenter.defaultCenter().addObserver(self, selector: "clearMemoryCache", name: UIApplicationDidReceiveMemoryWarningNotification, object: nil)
  78. NSNotificationCenter.defaultCenter().addObserver(self, selector: "cleanExpiredDiskCache", name: UIApplicationWillTerminateNotification, object: nil)
  79. NSNotificationCenter.defaultCenter().addObserver(self, selector: "backgroundCleanExpiredDiskCache", name: UIApplicationDidEnterBackgroundNotification, object: nil)
  80. }
  81. deinit {
  82. NSNotificationCenter.defaultCenter().removeObserver(self)
  83. }
  84. }
  85. // MARK: - Store & Remove
  86. public extension ImageCache {
  87. /**
  88. Store an image to cache. It will be saved to both memory and disk.
  89. It is an async operation, if you need to do something about the stored image, use `-storeImage:forKey:toDisk:completionHandler:`
  90. instead.
  91. :param: image The image will be stored.
  92. :param: key Key for the image.
  93. */
  94. public func storeImage(image: UIImage, forKey key: String) {
  95. storeImage(image, forKey: key, toDisk: true, completionHandler: nil)
  96. }
  97. /**
  98. Store an image to cache. It is an async operation.
  99. :param: image The image will be stored.
  100. :param: key Key for the image.
  101. :param: toDisk Whether this image should be cached to disk or not. If false, the image will be only cached in memory.
  102. :param: completionHandler Called when stroe operation completes.
  103. */
  104. public func storeImage(image: UIImage, forKey key: String, toDisk: Bool, completionHandler: (() -> ())?) {
  105. memoryCache.setObject(image, forKey: key, cost: image.kf_imageCost)
  106. if toDisk {
  107. dispatch_async(ioQueue, { () -> Void in
  108. if let data = UIImagePNGRepresentation(image) {
  109. if !self.fileManager.fileExistsAtPath(self.diskCachePath) {
  110. self.fileManager.createDirectoryAtPath(self.diskCachePath, withIntermediateDirectories: true, attributes: nil, error: nil)
  111. }
  112. self.fileManager.createFileAtPath(self.cachePathForKey(key), contents: data, attributes: nil)
  113. if let handler = completionHandler {
  114. dispatch_async(dispatch_get_main_queue()) {
  115. handler()
  116. }
  117. }
  118. } else {
  119. if let handler = completionHandler {
  120. dispatch_async(dispatch_get_main_queue()) {
  121. handler()
  122. }
  123. }
  124. }
  125. })
  126. } else {
  127. if let handler = completionHandler {
  128. handler()
  129. }
  130. }
  131. }
  132. /**
  133. Remove the image for key for the cache. It will be opted out from both memory and disk.
  134. It is an async operation, if you need to do something about the stored image, use `-removeImageForKey:fromDisk:completionHandler:`
  135. instead.
  136. :param: key Key for the image.
  137. */
  138. public func removeImageForKey(key: String) {
  139. removeImageForKey(key, fromDisk: true, completionHandler: nil)
  140. }
  141. /**
  142. Remove the image for key for the cache. It is an async operation.
  143. :param: key Key for the image.
  144. :param: fromDisk Whether this image should be removed from disk or not. If false, the image will be only removed from memory.
  145. :param: completionHandler Called when removal operation completes.
  146. */
  147. public func removeImageForKey(key: String, fromDisk: Bool, completionHandler: (() -> ())?) {
  148. memoryCache.removeObjectForKey(key)
  149. if fromDisk {
  150. dispatch_async(ioQueue, { () -> Void in
  151. self.fileManager.removeItemAtPath(self.cachePathForKey(key), error: nil)
  152. if let handler = completionHandler {
  153. dispatch_async(dispatch_get_main_queue()) {
  154. handler()
  155. }
  156. }
  157. })
  158. } else {
  159. if let handler = completionHandler {
  160. handler()
  161. }
  162. }
  163. }
  164. }
  165. // MARK: - Get data from cache
  166. extension ImageCache {
  167. /**
  168. Get an image for a key from memory or disk.
  169. :param: key Key for the image.
  170. :param: options Options of retriving image.
  171. :param: completionHandler Called when getting operation completes with image result and cached type of this image. If there is no such key cached, the image will be `nil`.
  172. :returns: The retriving task.
  173. */
  174. public func retrieveImageForKey(key: String, options:KingfisherManager.Options, completionHandler: ((UIImage?, CacheType!) -> ())?) -> RetrieveImageDiskTask? {
  175. // No completion handler. Not start working and early return.
  176. if (completionHandler == nil) {
  177. return dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) {}
  178. }
  179. let block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) {
  180. if let image = self.retrieveImageInMemoryCacheForKey(key) {
  181. //Found image in memory cache.
  182. if options.shouldDecode {
  183. dispatch_async(self.processQueue, { () -> Void in
  184. let result = image.kf_decodedImage()
  185. dispatch_async(dispatch_get_main_queue(), { () -> Void in
  186. completionHandler?(result, .Memory)
  187. return
  188. })
  189. })
  190. } else {
  191. completionHandler?(image, .Memory)
  192. }
  193. } else {
  194. //Begin to load image from disk
  195. dispatch_async(self.ioQueue, { () -> Void in
  196. if let image = self.retrieveImageInDiskCacheForKey(key) {
  197. if options.shouldDecode {
  198. dispatch_async(self.processQueue, { () -> Void in
  199. let result = image.kf_decodedImage()
  200. self.storeImage(result!, forKey: key, toDisk: false, completionHandler: nil)
  201. dispatch_async(dispatch_get_main_queue(), { () -> Void in
  202. completionHandler?(result, .Memory)
  203. return
  204. })
  205. })
  206. } else {
  207. self.storeImage(image, forKey: key, toDisk: false, completionHandler: nil)
  208. dispatch_async(dispatch_get_main_queue(), { () -> Void in
  209. if let completionHandler = completionHandler {
  210. completionHandler(image, .Disk)
  211. }
  212. })
  213. }
  214. } else {
  215. // No image found from either memory or disk
  216. dispatch_async(dispatch_get_main_queue(), { () -> Void in
  217. if let completionHandler = completionHandler {
  218. completionHandler(nil, nil)
  219. }
  220. })
  221. }
  222. })
  223. }
  224. }
  225. dispatch_async(dispatch_get_main_queue(), block)
  226. return block
  227. }
  228. /**
  229. Get an image for a key from memory.
  230. :param: key Key for the image.
  231. :returns: The image object if it is cached, or `nil` if there is no such key in the cache.
  232. */
  233. public func retrieveImageInMemoryCacheForKey(key: String) -> UIImage? {
  234. return memoryCache.objectForKey(key) as? UIImage
  235. }
  236. /**
  237. Get an image for a key from disk.
  238. :param: key Key for the image.
  239. :returns: The image object if it is cached, or `nil` if there is no such key in the cache.
  240. */
  241. public func retrieveImageInDiskCacheForKey(key: String) -> UIImage? {
  242. return diskImageForKey(key)
  243. }
  244. }
  245. // MARK: - Clear & Clean
  246. extension ImageCache {
  247. /**
  248. Clear memory cache.
  249. */
  250. @objc public func clearMemoryCache() {
  251. memoryCache.removeAllObjects()
  252. }
  253. /**
  254. Clear disk cache. This is an async operation.
  255. */
  256. public func clearDiskCache() {
  257. clearDiskCacheWithCompletionHandler(nil)
  258. }
  259. /**
  260. Clear disk cache. This is an async operation.
  261. :param: completionHander Called after the operation completes.
  262. */
  263. public func clearDiskCacheWithCompletionHandler(completionHander: (()->())?) {
  264. dispatch_async(ioQueue, { () -> Void in
  265. self.fileManager.removeItemAtPath(self.diskCachePath, error: nil)
  266. self.fileManager.createDirectoryAtPath(self.diskCachePath, withIntermediateDirectories: true, attributes: nil, error: nil)
  267. if let handler = completionHander {
  268. dispatch_async(dispatch_get_main_queue(), { () -> Void in
  269. handler()
  270. })
  271. }
  272. })
  273. }
  274. /**
  275. Clean expired disk cache. This is an async operation.
  276. */
  277. @objc public func cleanExpiredDiskCache() {
  278. cleanExpiredDiskCacheWithCompletionHander(nil)
  279. }
  280. /**
  281. Clean expired disk cache. This is an async operation.
  282. :param: completionHandler Called after the operation completes.
  283. */
  284. public func cleanExpiredDiskCacheWithCompletionHander(completionHandler: (()->())?) {
  285. // Do things in cocurrent io queue
  286. dispatch_async(ioQueue, { () -> Void in
  287. if let diskCacheURL = NSURL(fileURLWithPath: self.diskCachePath) {
  288. let resourceKeys = [NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]
  289. let expiredDate = NSDate(timeIntervalSinceNow: -self.maxCachePeriodInSecond)
  290. var cachedFiles = [NSURL: [NSObject: AnyObject]]()
  291. var URLsToDelete = [NSURL]()
  292. var diskCacheSize: UInt = 0
  293. if let fileEnumerator = self.fileManager.enumeratorAtURL(diskCacheURL,
  294. includingPropertiesForKeys: resourceKeys,
  295. options: NSDirectoryEnumerationOptions.SkipsHiddenFiles,
  296. errorHandler: nil) {
  297. for fileURL in fileEnumerator.allObjects as! [NSURL] {
  298. if let resourceValues = fileURL.resourceValuesForKeys(resourceKeys, error: nil) {
  299. // If it is a Directory. Continue to next file URL.
  300. if let isDirectory = resourceValues[NSURLIsDirectoryKey]?.boolValue {
  301. if isDirectory {
  302. continue
  303. }
  304. }
  305. // If this file is expired, add it to URLsToDelete
  306. if let modificationDate = resourceValues[NSURLContentModificationDateKey] as? NSDate {
  307. if modificationDate.laterDate(expiredDate) == expiredDate {
  308. URLsToDelete.append(fileURL)
  309. continue
  310. }
  311. }
  312. if let fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
  313. diskCacheSize += fileSize.unsignedLongValue
  314. cachedFiles[fileURL] = resourceValues
  315. }
  316. }
  317. }
  318. }
  319. for fileURL in URLsToDelete {
  320. self.fileManager.removeItemAtURL(fileURL, error: nil)
  321. }
  322. if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
  323. let targetSize = self.maxDiskCacheSize / 2
  324. // Sort files by last modify date. We want to clean from the oldest files.
  325. let sortedFiles = cachedFiles.keysSortedByValue({ (resourceValue1, resourceValue2) -> Bool in
  326. if let date1 = resourceValue1[NSURLContentModificationDateKey] as? NSDate {
  327. if let date2 = resourceValue2[NSURLContentModificationDateKey] as? NSDate {
  328. return date1.compare(date2) == .OrderedAscending
  329. }
  330. }
  331. // Not valid date information. This should not happen. Just in case.
  332. return true
  333. })
  334. for fileURL in sortedFiles {
  335. if (self.fileManager.removeItemAtURL(fileURL, error: nil)) {
  336. if let fileSize = cachedFiles[fileURL]?[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
  337. diskCacheSize -= fileSize.unsignedLongValue
  338. }
  339. if diskCacheSize < targetSize {
  340. break
  341. }
  342. }
  343. }
  344. }
  345. dispatch_async(dispatch_get_main_queue(), { () -> Void in
  346. if let completionHandler = completionHandler {
  347. completionHandler()
  348. }
  349. })
  350. } else {
  351. println("Bad disk cache path. \(self.diskCachePath) is not a valid local directory path.")
  352. }
  353. })
  354. }
  355. /**
  356. Clean expired disk cache when app in background. This is an async operation.
  357. In most cases, you should not call this method explicitly.
  358. It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received.
  359. */
  360. @objc public func backgroundCleanExpiredDiskCache() {
  361. func endBackgroundTask(inout task: UIBackgroundTaskIdentifier) {
  362. UIApplication.sharedApplication().endBackgroundTask(task)
  363. task = UIBackgroundTaskInvalid
  364. }
  365. var backgroundTask: UIBackgroundTaskIdentifier!
  366. backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { () -> Void in
  367. endBackgroundTask(&backgroundTask!)
  368. }
  369. cleanExpiredDiskCacheWithCompletionHander { () -> () in
  370. endBackgroundTask(&backgroundTask!)
  371. }
  372. }
  373. }
  374. // MARK: - Check cache status
  375. public extension ImageCache {
  376. /**
  377. * Cache result for checking whether an image is cached for a key.
  378. */
  379. public struct CacheCheckResult {
  380. public let cached: Bool
  381. public let cacheType: CacheType?
  382. }
  383. /**
  384. Check whether an image is cached for a key.
  385. :param: key Key for the image.
  386. :returns: The check result.
  387. */
  388. public func isImageCachedForKey(key: String) -> CacheCheckResult {
  389. if memoryCache.objectForKey(key) != nil {
  390. return CacheCheckResult(cached: true, cacheType: .Memory)
  391. }
  392. let filePath = cachePathForKey(key)
  393. if fileManager.fileExistsAtPath(filePath) {
  394. return CacheCheckResult(cached: true, cacheType: .Disk)
  395. }
  396. return CacheCheckResult(cached: false, cacheType: nil)
  397. }
  398. }
  399. // MARK: - Internal Helper
  400. extension ImageCache {
  401. func diskImageForKey(key: String) -> UIImage? {
  402. if let data = diskImageDataForKey(key) {
  403. if let image = UIImage(data: data) {
  404. return image
  405. } else {
  406. return nil
  407. }
  408. } else {
  409. return nil
  410. }
  411. }
  412. func diskImageDataForKey(key: String) -> NSData? {
  413. let filePath = cachePathForKey(key)
  414. return NSData(contentsOfFile: filePath)
  415. }
  416. func cachePathForKey(key: String) -> String {
  417. let fileName = cacheFileNameForKey(key)
  418. return diskCachePath.stringByAppendingPathComponent(fileName)
  419. }
  420. func cacheFileNameForKey(key: String) -> String {
  421. return key.kf_MD5()
  422. }
  423. }
  424. extension UIImage {
  425. var kf_imageCost: Int {
  426. return Int(size.height * size.width * scale * scale)
  427. }
  428. }
  429. extension Dictionary {
  430. func keysSortedByValue(isOrderedBefore:(Value, Value) -> Bool) -> [Key] {
  431. var array = Array(self)
  432. sort(&array) {
  433. let (lk, lv) = $0
  434. let (rk, rv) = $1
  435. return isOrderedBefore(lv, rv)
  436. }
  437. return array.map {
  438. let (k, v) = $0
  439. return k
  440. }
  441. }
  442. }