5 // Created by Wei Wang on 15/4/6.
7 // Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
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:
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
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
33 public extension Notification.Name {
35 This notification will be sent when the disk cache got cleaned either there are cached files expired or the total size exceeding the max allowed size. The manually invoking of `clearDiskCache` method will not trigger this notification.
37 The `object` of this notification is the `ImageCache` object which sends the notification.
39 A list of removed hashes (files) could be retrieved by accessing the array under `KingfisherDiskCacheCleanedHashKey` key in `userInfo` of the notification object you received. By checking the array, you could know the hash codes of files are removed.
41 The main purpose of this notification is supplying a chance to maintain some necessary information on the cached files. See [this wiki](https://github.com/onevcat/Kingfisher/wiki/How-to-implement-ETag-based-304-(Not-Modified)-handling-in-Kingfisher) for a use case on it.
43 public static var KingfisherDidCleanDiskCache = Notification.Name.init("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache")
47 Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`.
49 public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash"
51 /// It represents a task of retrieving image. You can call `cancel` on it to stop the process.
52 public typealias RetrieveImageDiskTask = DispatchWorkItem
55 Cache type of a cached image.
57 - None: The image is not cached yet when retrieving it.
58 - Memory: The image is cached in memory.
59 - Disk: The image is cached in disk.
61 public enum CacheType {
62 case none, memory, disk
64 public var cached: Bool {
66 case .memory, .disk: return true
67 case .none: return false
72 /// `ImageCache` represents both the memory and disk cache system of Kingfisher.
73 /// While a default image cache object will be used if you prefer the extension methods of Kingfisher,
74 /// you can create your own cache object and configure it as your need. You could use an `ImageCache`
75 /// object to manipulate memory and disk cache for Kingfisher.
76 open class ImageCache {
79 fileprivate let memoryCache = NSCache<NSString, AnyObject>()
81 /// The largest cache cost of memory cache. The total cost is pixel count of
82 /// all cached images in memory.
83 /// Default is unlimited. Memory cache will be purged automatically when a
84 /// memory warning notification is received.
85 open var maxMemoryCost: UInt = 0 {
87 self.memoryCache.totalCostLimit = Int(maxMemoryCost)
92 fileprivate let ioQueue: DispatchQueue
93 fileprivate var fileManager: FileManager!
95 ///The disk cache location.
96 open let diskCachePath: String
98 /// The default file extension appended to cached files.
99 open var pathExtension: String?
101 /// The longest time duration in second of the cache being stored in disk.
102 /// Default is 1 week (60 * 60 * 24 * 7 seconds).
103 /// Setting this to a negative value will make the disk cache never expiring.
104 open var maxCachePeriodInSecond: TimeInterval = 60 * 60 * 24 * 7 //Cache exists for 1 week
106 /// The largest disk size can be taken for the cache. It is the total
107 /// allocated size of cached files in bytes.
108 /// Default is no limit.
109 open var maxDiskCacheSize: UInt = 0
111 fileprivate let processQueue: DispatchQueue
113 /// The default cache.
114 public static let `default` = ImageCache(name: "default")
116 /// Closure that defines the disk cache path from a given path and cacheName.
117 public typealias DiskCachePathClosure = (String?, String) -> String
119 /// The default DiskCachePathClosure
120 public final class func defaultDiskCachePathClosure(path: String?, cacheName: String) -> String {
121 let dstPath = path ?? NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
122 return (dstPath as NSString).appendingPathComponent(cacheName)
126 Init method. Passing a name for the cache. It represents a cache folder in the memory and disk.
128 - parameter name: Name of the cache. It will be used as the memory cache name and the disk cache folder name
129 appending to the cache path. This value should not be an empty string.
130 - parameter path: Optional - Location of cache path on disk. If `nil` is passed in (the default value),
131 the `.cachesDirectory` in of your app will be used.
132 - parameter diskCachePathClosure: Closure that takes in an optional initial path string and generates
133 the final disk cache path. You could use it to fully customize your cache path.
135 - returns: The cache object.
137 public init(name: String,
139 diskCachePathClosure: DiskCachePathClosure = ImageCache.defaultDiskCachePathClosure)
143 fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
146 let cacheName = "com.onevcat.Kingfisher.ImageCache.\(name)"
147 memoryCache.name = cacheName
149 diskCachePath = diskCachePathClosure(path, cacheName)
151 let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(name)"
152 ioQueue = DispatchQueue(label: ioQueueName)
154 let processQueueName = "com.onevcat.Kingfisher.ImageCache.processQueue.\(name)"
155 processQueue = DispatchQueue(label: processQueueName, attributes: .concurrent)
157 ioQueue.sync { fileManager = FileManager() }
159 #if !os(macOS) && !os(watchOS)
160 NotificationCenter.default.addObserver(
161 self, selector: #selector(clearMemoryCache), name: .UIApplicationDidReceiveMemoryWarning, object: nil)
162 NotificationCenter.default.addObserver(
163 self, selector: #selector(cleanExpiredDiskCache), name: .UIApplicationWillTerminate, object: nil)
164 NotificationCenter.default.addObserver(
165 self, selector: #selector(backgroundCleanExpiredDiskCache), name: .UIApplicationDidEnterBackground, object: nil)
170 NotificationCenter.default.removeObserver(self)
174 // MARK: - Store & Remove
177 Store an image to cache. It will be saved to both memory and disk. It is an async operation.
179 - parameter image: The image to be stored.
180 - parameter original: The original data of the image.
181 Kingfisher will use it to check the format of the image and optimize cache size on disk.
182 If `nil` is supplied, the image data will be saved as a normalized PNG file.
183 It is strongly suggested to supply it whenever possible, to get a better performance and disk usage.
184 - parameter key: Key for the image.
185 - parameter identifier: The identifier of processor used. If you are using a processor for the image, pass the identifier of
187 This identifier will be used to generate a corresponding key for the combination of `key` and processor.
188 - parameter toDisk: Whether this image should be cached to disk or not. If false, the image will be only cached in memory.
189 - parameter completionHandler: Called when store operation completes.
191 open func store(_ image: Image,
192 original: Data? = nil,
194 processorIdentifier identifier: String = "",
195 cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
197 completionHandler: (() -> Void)? = nil)
200 let computedKey = key.computedKey(with: identifier)
201 memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
203 func callHandlerInMainQueue() {
204 if let handler = completionHandler {
205 DispatchQueue.main.async {
214 if let data = serializer.data(with: image, original: original) {
215 if !self.fileManager.fileExists(atPath: self.diskCachePath) {
217 try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
221 self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
223 callHandlerInMainQueue()
226 callHandlerInMainQueue()
231 Remove the image for key for the cache. It will be opted out from both memory and disk.
232 It is an async operation.
234 - parameter key: Key for the image.
235 - parameter identifier: The identifier of processor used. If you are using a processor for the image, pass the identifier of processor to it.
236 This identifier will be used to generate a corresponding key for the combination of `key` and processor.
237 - parameter fromMemory: Whether this image should be removed from memory or not. If false, the image won't be removed from memory.
238 - parameter fromDisk: Whether this image should be removed from disk or not. If false, the image won't be removed from disk.
239 - parameter completionHandler: Called when removal operation completes.
241 open func removeImage(forKey key: String,
242 processorIdentifier identifier: String = "",
243 fromMemory: Bool = true,
244 fromDisk: Bool = true,
245 completionHandler: (() -> Void)? = nil)
247 let computedKey = key.computedKey(with: identifier)
250 memoryCache.removeObject(forKey: computedKey as NSString)
253 func callHandlerInMainQueue() {
254 if let handler = completionHandler {
255 DispatchQueue.main.async {
264 try self.fileManager.removeItem(atPath: self.cachePath(forComputedKey: computedKey))
266 callHandlerInMainQueue()
269 callHandlerInMainQueue()
273 // MARK: - Get data from cache
276 Get an image for a key from memory or disk.
278 - parameter key: Key for the image.
279 - parameter options: Options of retrieving image. If you need to retrieve an image which was
280 stored with a specified `ImageProcessor`, pass the processor in the option too.
281 - parameter completionHandler: Called when getting operation completes with image result and cached type of
282 this image. If there is no such key cached, the image will be `nil`.
284 - returns: The retrieving task.
287 open func retrieveImage(forKey key: String,
288 options: KingfisherOptionsInfo?,
289 completionHandler: ((Image?, CacheType) -> Void)?) -> RetrieveImageDiskTask?
291 // No completion handler. Not start working and early return.
292 guard let completionHandler = completionHandler else {
296 var block: RetrieveImageDiskTask?
297 let options = options ?? KingfisherEmptyOptionsInfo
298 let imageModifier = options.imageModifier
300 if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
301 options.callbackDispatchQueue.safeAsync {
302 completionHandler(imageModifier.modify(image), .memory)
304 } else if options.fromMemoryCacheOrRefresh { // Only allows to get images from memory cache.
305 options.callbackDispatchQueue.safeAsync {
306 completionHandler(nil, .none)
309 var sSelf: ImageCache! = self
310 block = DispatchWorkItem(block: {
311 // Begin to load image from disk
312 if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options) {
313 if options.backgroundDecode {
314 sSelf.processQueue.async {
316 let result = image.kf.decoded
320 processorIdentifier: options.processor.identifier,
321 cacheSerializer: options.cacheSerializer,
323 completionHandler: nil)
324 options.callbackDispatchQueue.safeAsync {
325 completionHandler(imageModifier.modify(result), .memory)
332 processorIdentifier: options.processor.identifier,
333 cacheSerializer: options.cacheSerializer,
335 completionHandler: nil
337 options.callbackDispatchQueue.safeAsync {
338 completionHandler(imageModifier.modify(image), .disk)
343 // No image found from either memory or disk
344 options.callbackDispatchQueue.safeAsync {
345 completionHandler(nil, .none)
351 sSelf.ioQueue.async(execute: block!)
358 Get an image for a key from memory.
360 - parameter key: Key for the image.
361 - parameter options: Options of retrieving image. If you need to retrieve an image which was
362 stored with a specified `ImageProcessor`, pass the processor in the option too.
363 - returns: The image object if it is cached, or `nil` if there is no such key in the cache.
365 open func retrieveImageInMemoryCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
367 let options = options ?? KingfisherEmptyOptionsInfo
368 let computedKey = key.computedKey(with: options.processor.identifier)
370 return memoryCache.object(forKey: computedKey as NSString) as? Image
374 Get an image for a key from disk.
376 - parameter key: Key for the image.
377 - parameter options: Options of retrieving image. If you need to retrieve an image which was
378 stored with a specified `ImageProcessor`, pass the processor in the option too.
380 - returns: The image object if it is cached, or `nil` if there is no such key in the cache.
382 open func retrieveImageInDiskCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
384 let options = options ?? KingfisherEmptyOptionsInfo
385 let computedKey = key.computedKey(with: options.processor.identifier)
387 return diskImage(forComputedKey: computedKey, serializer: options.cacheSerializer, options: options)
391 // MARK: - Clear & Clean
396 @objc public func clearMemoryCache() {
397 memoryCache.removeAllObjects()
401 Clear disk cache. This is an async operation.
403 - parameter completionHander: Called after the operation completes.
405 open func clearDiskCache(completion handler: (()->())? = nil) {
408 try self.fileManager.removeItem(atPath: self.diskCachePath)
409 try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
412 if let handler = handler {
413 DispatchQueue.main.async {
421 Clean expired disk cache. This is an async operation.
423 @objc fileprivate func cleanExpiredDiskCache() {
424 cleanExpiredDiskCache(completion: nil)
428 Clean expired disk cache. This is an async operation.
430 - parameter completionHandler: Called after the operation completes.
432 open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {
434 // Do things in cocurrent io queue
437 var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
439 for fileURL in URLsToDelete {
441 try self.fileManager.removeItem(at: fileURL)
445 if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
446 let targetSize = self.maxDiskCacheSize / 2
448 // Sort files by last modify date. We want to clean from the oldest files.
449 let sortedFiles = cachedFiles.keysSortedByValue {
450 resourceValue1, resourceValue2 -> Bool in
452 if let date1 = resourceValue1.contentAccessDate,
453 let date2 = resourceValue2.contentAccessDate
455 return date1.compare(date2) == .orderedAscending
458 // Not valid date information. This should not happen. Just in case.
462 for fileURL in sortedFiles {
465 try self.fileManager.removeItem(at: fileURL)
468 URLsToDelete.append(fileURL)
470 if let fileSize = cachedFiles[fileURL]?.totalFileAllocatedSize {
471 diskCacheSize -= UInt(fileSize)
474 if diskCacheSize < targetSize {
480 DispatchQueue.main.async {
482 if URLsToDelete.count != 0 {
483 let cleanedHashes = URLsToDelete.map { $0.lastPathComponent }
484 NotificationCenter.default.post(name: .KingfisherDidCleanDiskCache, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
492 fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
494 let diskCacheURL = URL(fileURLWithPath: diskCachePath)
495 let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
496 let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
498 var cachedFiles = [URL: URLResourceValues]()
499 var urlsToDelete = [URL]()
500 var diskCacheSize: UInt = 0
502 for fileUrl in (try? fileManager.contentsOfDirectory(at: diskCacheURL, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)) ?? [] {
505 let resourceValues = try fileUrl.resourceValues(forKeys: resourceKeys)
506 // If it is a Directory. Continue to next file URL.
507 if resourceValues.isDirectory == true {
511 // If this file is expired, add it to URLsToDelete
512 if !onlyForCacheSize,
513 let expiredDate = expiredDate,
514 let lastAccessData = resourceValues.contentAccessDate,
515 (lastAccessData as NSDate).laterDate(expiredDate) == expiredDate
517 urlsToDelete.append(fileUrl)
521 if let fileSize = resourceValues.totalFileAllocatedSize {
522 diskCacheSize += UInt(fileSize)
523 if !onlyForCacheSize {
524 cachedFiles[fileUrl] = resourceValues
530 return (urlsToDelete, diskCacheSize, cachedFiles)
533 #if !os(macOS) && !os(watchOS)
535 Clean expired disk cache when app in background. This is an async operation.
536 In most cases, you should not call this method explicitly.
537 It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received.
539 @objc public func backgroundCleanExpiredDiskCache() {
540 // if 'sharedApplication()' is unavailable, then return
541 guard let sharedApplication = Kingfisher<UIApplication>.shared else { return }
543 func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) {
544 sharedApplication.endBackgroundTask(task)
545 task = UIBackgroundTaskInvalid
548 var backgroundTask: UIBackgroundTaskIdentifier!
549 backgroundTask = sharedApplication.beginBackgroundTask {
550 endBackgroundTask(&backgroundTask!)
553 cleanExpiredDiskCache {
554 endBackgroundTask(&backgroundTask!)
560 // MARK: - Check cache status
562 /// Cache type for checking whether an image is cached for a key in current cache.
565 /// - key: Key for the image.
566 /// - identifier: Processor identifier which used for this image. Default is empty string.
567 /// - Returns: A `CacheType` instance which indicates the cache status. `.none` means the image is not in cache yet.
568 open func imageCachedType(forKey key: String, processorIdentifier identifier: String = "") -> CacheType {
569 let computedKey = key.computedKey(with: identifier)
571 if memoryCache.object(forKey: computedKey as NSString) != nil {
575 let filePath = cachePath(forComputedKey: computedKey)
577 var diskCached = false
579 diskCached = fileManager.fileExists(atPath: filePath)
590 Get the hash for the key. This could be used for matching files.
592 - parameter key: The key which is used for caching.
593 - parameter identifier: The identifier of processor used. If you are using a processor for the image, pass the identifier of processor to it.
595 - returns: Corresponding hash.
597 open func hash(forKey key: String, processorIdentifier identifier: String = "") -> String {
598 let computedKey = key.computedKey(with: identifier)
599 return cacheFileName(forComputedKey: computedKey)
603 Calculate the disk size taken by cache.
604 It is the total allocated size of the cached files in bytes.
606 - parameter completionHandler: Called with the calculated size when finishes.
608 open func calculateDiskCacheSize(completion handler: @escaping ((_ size: UInt) -> Void)) {
610 let (_, diskCacheSize, _) = self.travelCachedFiles(onlyForCacheSize: true)
611 DispatchQueue.main.async {
612 handler(diskCacheSize)
618 Get the cache path for the key.
619 It is useful for projects with UIWebView or anyone that needs access to the local file path.
621 i.e. Replace the `<img src='path_for_key'>` tag in your HTML.
623 - Note: This method does not guarantee there is an image already cached in the path. It just returns the path
624 that the image should be.
625 You could use `isImageCached(forKey:)` method to check whether the image is cached under that key.
627 open func cachePath(forKey key: String, processorIdentifier identifier: String = "") -> String {
628 let computedKey = key.computedKey(with: identifier)
629 return cachePath(forComputedKey: computedKey)
632 open func cachePath(forComputedKey key: String) -> String {
633 let fileName = cacheFileName(forComputedKey: key)
634 return (diskCachePath as NSString).appendingPathComponent(fileName)
638 // MARK: - Internal Helper
639 extension ImageCache {
641 func diskImage(forComputedKey key: String, serializer: CacheSerializer, options: KingfisherOptionsInfo) -> Image? {
642 if let data = diskImageData(forComputedKey: key) {
643 return serializer.image(with: data, options: options)
649 func diskImageData(forComputedKey key: String) -> Data? {
650 let filePath = cachePath(forComputedKey: key)
651 return (try? Data(contentsOf: URL(fileURLWithPath: filePath)))
654 func cacheFileName(forComputedKey key: String) -> String {
655 if let ext = self.pathExtension {
656 return (key.kf.md5 as NSString).appendingPathExtension(ext)!
662 // MARK: - Deprecated
663 extension ImageCache {
665 * Cache result for checking whether an image is cached for a key.
667 @available(*, deprecated,
668 message: "CacheCheckResult is deprecated. Use imageCachedType(forKey:processorIdentifier:) API instead.")
669 public struct CacheCheckResult {
670 public let cached: Bool
671 public let cacheType: CacheType?
675 Check whether an image is cached for a key.
677 - parameter key: Key for the image.
679 - returns: The check result.
681 @available(*, deprecated,
682 message: "Use imageCachedType(forKey:processorIdentifier:) instead. CacheCheckResult.none indicates not being cached.",
683 renamed: "imageCachedType(forKey:processorIdentifier:)")
684 open func isImageCached(forKey key: String, processorIdentifier identifier: String = "") -> CacheCheckResult {
685 let result = imageCachedType(forKey: key, processorIdentifier: identifier)
688 return CacheCheckResult(cached: true, cacheType: result)
690 return CacheCheckResult(cached: false, cacheType: nil)
695 extension Kingfisher where Base: Image {
697 return images == nil ?
698 Int(size.height * size.width * scale * scale) :
699 Int(size.height * size.width * scale * scale) * images!.count
703 extension Dictionary {
704 func keysSortedByValue(_ isOrderedBefore: (Value, Value) -> Bool) -> [Key] {
705 return Array(self).sorted{ isOrderedBefore($0.1, $1.1) }.map{ $0.0 }
709 #if !os(macOS) && !os(watchOS)
710 // MARK: - For App Extensions
711 extension UIApplication: KingfisherCompatible { }
712 extension Kingfisher where Base: UIApplication {
713 public static var shared: UIApplication? {
714 let selector = NSSelectorFromString("sharedApplication")
715 guard Base.responds(to: selector) else { return nil }
716 return Base.perform(selector).takeUnretainedValue() as? UIApplication
722 func computedKey(with identifier: String) -> String {
723 if identifier.isEmpty {
726 return appending("@\(identifier)")