added iOS source code
[wl-app.git] / iOS / Pods / Kingfisher / Sources / ImageCache.swift
1 //
2 //  ImageCache.swift
3 //  Kingfisher
4 //
5 //  Created by Wei Wang on 15/4/6.
6 //
7 //  Copyright (c) 2018 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
27 #if os(macOS)
28 import AppKit
29 #else
30 import UIKit
31 #endif
32
33 public extension Notification.Name {
34     /**
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.
36      
37      The `object` of this notification is the `ImageCache` object which sends the notification.
38      
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.
40      
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.
42      */
43     public static var KingfisherDidCleanDiskCache = Notification.Name.init("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache")
44 }
45
46 /**
47 Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`.
48 */
49 public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash"
50
51 /// It represents a task of retrieving image. You can call `cancel` on it to stop the process.
52 public typealias RetrieveImageDiskTask = DispatchWorkItem
53
54 /**
55 Cache type of a cached image.
56
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.
60 */
61 public enum CacheType {
62     case none, memory, disk
63     
64     public var cached: Bool {
65         switch self {
66         case .memory, .disk: return true
67         case .none: return false
68         }
69     }
70 }
71
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 {
77
78     //Memory
79     fileprivate let memoryCache = NSCache<NSString, AnyObject>()
80     
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 {
86         didSet {
87             self.memoryCache.totalCostLimit = Int(maxMemoryCost)
88         }
89     }
90     
91     //Disk
92     fileprivate let ioQueue: DispatchQueue
93     fileprivate var fileManager: FileManager!
94     
95     ///The disk cache location.
96     open let diskCachePath: String
97   
98     /// The default file extension appended to cached files.
99     open var pathExtension: String?
100     
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
105     
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
110     
111     fileprivate let processQueue: DispatchQueue
112     
113     /// The default cache.
114     public static let `default` = ImageCache(name: "default")
115     
116     /// Closure that defines the disk cache path from a given path and cacheName.
117     public typealias DiskCachePathClosure = (String?, String) -> String
118     
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)
123     }
124     
125     /**
126     Init method. Passing a name for the cache. It represents a cache folder in the memory and disk.
127     
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.
134     
135     - returns: The cache object.
136     */
137     public init(name: String,
138                 path: String? = nil,
139                 diskCachePathClosure: DiskCachePathClosure = ImageCache.defaultDiskCachePathClosure)
140     {
141         
142         if name.isEmpty {
143             fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
144         }
145         
146         let cacheName = "com.onevcat.Kingfisher.ImageCache.\(name)"
147         memoryCache.name = cacheName
148         
149         diskCachePath = diskCachePathClosure(path, cacheName)
150         
151         let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(name)"
152         ioQueue = DispatchQueue(label: ioQueueName)
153         
154         let processQueueName = "com.onevcat.Kingfisher.ImageCache.processQueue.\(name)"
155         processQueue = DispatchQueue(label: processQueueName, attributes: .concurrent)
156         
157         ioQueue.sync { fileManager = FileManager() }
158         
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)
166 #endif
167     }
168     
169     deinit {
170         NotificationCenter.default.removeObserver(self)
171     }
172
173
174     // MARK: - Store & Remove
175
176     /**
177     Store an image to cache. It will be saved to both memory and disk. It is an async operation.
178     
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
186                                    processor to it.
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.
190     */
191     open func store(_ image: Image,
192                       original: Data? = nil,
193                       forKey key: String,
194                       processorIdentifier identifier: String = "",
195                       cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
196                       toDisk: Bool = true,
197                       completionHandler: (() -> Void)? = nil)
198     {
199         
200         let computedKey = key.computedKey(with: identifier)
201         memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
202
203         func callHandlerInMainQueue() {
204             if let handler = completionHandler {
205                 DispatchQueue.main.async {
206                     handler()
207                 }
208             }
209         }
210         
211         if toDisk {
212             ioQueue.async {
213                 
214                 if let data = serializer.data(with: image, original: original) {
215                     if !self.fileManager.fileExists(atPath: self.diskCachePath) {
216                         do {
217                             try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
218                         } catch _ {}
219                     }
220                     
221                     self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
222                 }
223                 callHandlerInMainQueue()
224             }
225         } else {
226             callHandlerInMainQueue()
227         }
228     }
229     
230     /**
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.
233     
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.
240     */
241     open func removeImage(forKey key: String,
242                           processorIdentifier identifier: String = "",
243                           fromMemory: Bool = true,
244                           fromDisk: Bool = true,
245                           completionHandler: (() -> Void)? = nil)
246     {
247         let computedKey = key.computedKey(with: identifier)
248
249         if fromMemory {
250             memoryCache.removeObject(forKey: computedKey as NSString)
251         }
252         
253         func callHandlerInMainQueue() {
254             if let handler = completionHandler {
255                 DispatchQueue.main.async {
256                     handler()
257                 }
258             }
259         }
260         
261         if fromDisk {
262             ioQueue.async{
263                 do {
264                     try self.fileManager.removeItem(atPath: self.cachePath(forComputedKey: computedKey))
265                 } catch _ {}
266                 callHandlerInMainQueue()
267             }
268         } else {
269             callHandlerInMainQueue()
270         }
271     }
272
273     // MARK: - Get data from cache
274
275     /**
276     Get an image for a key from memory or disk.
277     
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`.
283     
284     - returns: The retrieving task.
285     */
286     @discardableResult
287     open func retrieveImage(forKey key: String,
288                                options: KingfisherOptionsInfo?,
289                      completionHandler: ((Image?, CacheType) -> Void)?) -> RetrieveImageDiskTask?
290     {
291         // No completion handler. Not start working and early return.
292         guard let completionHandler = completionHandler else {
293             return nil
294         }
295         
296         var block: RetrieveImageDiskTask?
297         let options = options ?? KingfisherEmptyOptionsInfo
298         let imageModifier = options.imageModifier
299
300         if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
301             options.callbackDispatchQueue.safeAsync {
302                 completionHandler(imageModifier.modify(image), .memory)
303             }
304         } else if options.fromMemoryCacheOrRefresh { // Only allows to get images from memory cache.
305             options.callbackDispatchQueue.safeAsync {
306                 completionHandler(nil, .none)
307             }
308         } else {
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 {
315
316                             let result = image.kf.decoded
317                             
318                             sSelf.store(result,
319                                         forKey: key,
320                                         processorIdentifier: options.processor.identifier,
321                                         cacheSerializer: options.cacheSerializer,
322                                         toDisk: false,
323                                         completionHandler: nil)
324                             options.callbackDispatchQueue.safeAsync {
325                                 completionHandler(imageModifier.modify(result), .memory)
326                                 sSelf = nil
327                             }
328                         }
329                     } else {
330                         sSelf.store(image,
331                                     forKey: key,
332                                     processorIdentifier: options.processor.identifier,
333                                     cacheSerializer: options.cacheSerializer,
334                                     toDisk: false,
335                                     completionHandler: nil
336                         )
337                         options.callbackDispatchQueue.safeAsync {
338                             completionHandler(imageModifier.modify(image), .disk)
339                             sSelf = nil
340                         }
341                     }
342                 } else {
343                     // No image found from either memory or disk
344                     options.callbackDispatchQueue.safeAsync {
345                         completionHandler(nil, .none)
346                         sSelf = nil
347                     }
348                 }
349             })
350             
351             sSelf.ioQueue.async(execute: block!)
352         }
353     
354         return block
355     }
356     
357     /**
358     Get an image for a key from memory.
359     
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.
364     */
365     open func retrieveImageInMemoryCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
366         
367         let options = options ?? KingfisherEmptyOptionsInfo
368         let computedKey = key.computedKey(with: options.processor.identifier)
369         
370         return memoryCache.object(forKey: computedKey as NSString) as? Image
371     }
372     
373     /**
374     Get an image for a key from disk.
375     
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.
379
380     - returns: The image object if it is cached, or `nil` if there is no such key in the cache.
381     */
382     open func retrieveImageInDiskCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
383         
384         let options = options ?? KingfisherEmptyOptionsInfo
385         let computedKey = key.computedKey(with: options.processor.identifier)
386         
387         return diskImage(forComputedKey: computedKey, serializer: options.cacheSerializer, options: options)
388     }
389
390
391     // MARK: - Clear & Clean
392
393     /**
394     Clear memory cache.
395     */
396     @objc public func clearMemoryCache() {
397         memoryCache.removeAllObjects()
398     }
399     
400     /**
401     Clear disk cache. This is an async operation.
402     
403     - parameter completionHander: Called after the operation completes.
404     */
405     open func clearDiskCache(completion handler: (()->())? = nil) {
406         ioQueue.async {
407             do {
408                 try self.fileManager.removeItem(atPath: self.diskCachePath)
409                 try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
410             } catch _ { }
411             
412             if let handler = handler {
413                 DispatchQueue.main.async {
414                     handler()
415                 }
416             }
417         }
418     }
419     
420     /**
421     Clean expired disk cache. This is an async operation.
422     */
423     @objc fileprivate func cleanExpiredDiskCache() {
424         cleanExpiredDiskCache(completion: nil)
425     }
426     
427     /**
428     Clean expired disk cache. This is an async operation.
429     
430     - parameter completionHandler: Called after the operation completes.
431     */
432     open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {
433         
434         // Do things in cocurrent io queue
435         ioQueue.async {
436             
437             var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
438             
439             for fileURL in URLsToDelete {
440                 do {
441                     try self.fileManager.removeItem(at: fileURL)
442                 } catch _ { }
443             }
444                 
445             if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
446                 let targetSize = self.maxDiskCacheSize / 2
447                     
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
451                     
452                     if let date1 = resourceValue1.contentAccessDate,
453                        let date2 = resourceValue2.contentAccessDate
454                     {
455                         return date1.compare(date2) == .orderedAscending
456                     }
457                     
458                     // Not valid date information. This should not happen. Just in case.
459                     return true
460                 }
461                 
462                 for fileURL in sortedFiles {
463                     
464                     do {
465                         try self.fileManager.removeItem(at: fileURL)
466                     } catch { }
467                         
468                     URLsToDelete.append(fileURL)
469                     
470                     if let fileSize = cachedFiles[fileURL]?.totalFileAllocatedSize {
471                         diskCacheSize -= UInt(fileSize)
472                     }
473                     
474                     if diskCacheSize < targetSize {
475                         break
476                     }
477                 }
478             }
479                 
480             DispatchQueue.main.async {
481                 
482                 if URLsToDelete.count != 0 {
483                     let cleanedHashes = URLsToDelete.map { $0.lastPathComponent }
484                     NotificationCenter.default.post(name: .KingfisherDidCleanDiskCache, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
485                 }
486                 
487                 handler?()
488             }
489         }
490     }
491     
492     fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
493         
494         let diskCacheURL = URL(fileURLWithPath: diskCachePath)
495         let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
496         let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
497         
498         var cachedFiles = [URL: URLResourceValues]()
499         var urlsToDelete = [URL]()
500         var diskCacheSize: UInt = 0
501
502         for fileUrl in (try? fileManager.contentsOfDirectory(at: diskCacheURL, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)) ?? [] {
503
504             do {
505                 let resourceValues = try fileUrl.resourceValues(forKeys: resourceKeys)
506                 // If it is a Directory. Continue to next file URL.
507                 if resourceValues.isDirectory == true {
508                     continue
509                 }
510
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
516                 {
517                     urlsToDelete.append(fileUrl)
518                     continue
519                 }
520
521                 if let fileSize = resourceValues.totalFileAllocatedSize {
522                     diskCacheSize += UInt(fileSize)
523                     if !onlyForCacheSize {
524                         cachedFiles[fileUrl] = resourceValues
525                     }
526                 }
527             } catch _ { }
528         }
529
530         return (urlsToDelete, diskCacheSize, cachedFiles)
531     }
532
533 #if !os(macOS) && !os(watchOS)
534     /**
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.
538     */
539     @objc public func backgroundCleanExpiredDiskCache() {
540         // if 'sharedApplication()' is unavailable, then return
541         guard let sharedApplication = Kingfisher<UIApplication>.shared else { return }
542
543         func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) {
544             sharedApplication.endBackgroundTask(task)
545             task = UIBackgroundTaskInvalid
546         }
547         
548         var backgroundTask: UIBackgroundTaskIdentifier!
549         backgroundTask = sharedApplication.beginBackgroundTask {
550             endBackgroundTask(&backgroundTask!)
551         }
552         
553         cleanExpiredDiskCache {
554             endBackgroundTask(&backgroundTask!)
555         }
556     }
557 #endif
558
559
560     // MARK: - Check cache status
561     
562     /// Cache type for checking whether an image is cached for a key in current cache.
563     ///
564     /// - Parameters:
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)
570         
571         if memoryCache.object(forKey: computedKey as NSString) != nil {
572             return .memory
573         }
574         
575         let filePath = cachePath(forComputedKey: computedKey)
576         
577         var diskCached = false
578         ioQueue.sync {
579             diskCached = fileManager.fileExists(atPath: filePath)
580         }
581         
582         if diskCached {
583             return .disk
584         }
585         
586         return .none
587     }
588     
589     /**
590     Get the hash for the key. This could be used for matching files.
591     
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.
594     
595      - returns: Corresponding hash.
596     */
597     open func hash(forKey key: String, processorIdentifier identifier: String = "") -> String {
598         let computedKey = key.computedKey(with: identifier)
599         return cacheFileName(forComputedKey: computedKey)
600     }
601     
602     /**
603     Calculate the disk size taken by cache. 
604     It is the total allocated size of the cached files in bytes.
605     
606     - parameter completionHandler: Called with the calculated size when finishes.
607     */
608     open func calculateDiskCacheSize(completion handler: @escaping ((_ size: UInt) -> Void)) {
609         ioQueue.async {
610             let (_, diskCacheSize, _) = self.travelCachedFiles(onlyForCacheSize: true)
611             DispatchQueue.main.async {
612                 handler(diskCacheSize)
613             }
614         }
615     }
616     
617     /**
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.
620     
621     i.e. Replace the `<img src='path_for_key'>` tag in your HTML.
622      
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.
626     */
627     open func cachePath(forKey key: String, processorIdentifier identifier: String = "") -> String {
628         let computedKey = key.computedKey(with: identifier)
629         return cachePath(forComputedKey: computedKey)
630     }
631
632     open func cachePath(forComputedKey key: String) -> String {
633         let fileName = cacheFileName(forComputedKey: key)
634         return (diskCachePath as NSString).appendingPathComponent(fileName)
635     }
636 }
637
638 // MARK: - Internal Helper
639 extension ImageCache {
640   
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)
644         } else {
645             return nil
646         }
647     }
648     
649     func diskImageData(forComputedKey key: String) -> Data? {
650         let filePath = cachePath(forComputedKey: key)
651         return (try? Data(contentsOf: URL(fileURLWithPath: filePath)))
652     }
653     
654     func cacheFileName(forComputedKey key: String) -> String {
655         if let ext = self.pathExtension {
656           return (key.kf.md5 as NSString).appendingPathExtension(ext)!
657         }
658         return key.kf.md5
659     }
660 }
661
662 // MARK: - Deprecated
663 extension ImageCache {
664     /**
665      *  Cache result for checking whether an image is cached for a key.
666      */
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?
672     }
673     
674     /**
675      Check whether an image is cached for a key.
676      
677      - parameter key: Key for the image.
678      
679      - returns: The check result.
680      */
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)
686         switch result {
687         case .memory, .disk:
688             return CacheCheckResult(cached: true, cacheType: result)
689         case .none:
690             return CacheCheckResult(cached: false, cacheType: nil)
691         }
692     }
693 }
694
695 extension Kingfisher where Base: Image {
696     var imageCost: Int {
697         return images == nil ?
698             Int(size.height * size.width * scale * scale) :
699             Int(size.height * size.width * scale * scale) * images!.count
700     }
701 }
702
703 extension Dictionary {
704     func keysSortedByValue(_ isOrderedBefore: (Value, Value) -> Bool) -> [Key] {
705         return Array(self).sorted{ isOrderedBefore($0.1, $1.1) }.map{ $0.0 }
706     }
707 }
708
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
717     }
718 }
719 #endif
720
721 extension String {
722     func computedKey(with identifier: String) -> String {
723         if identifier.isEmpty {
724             return self
725         } else {
726             return appending("@\(identifier)")
727         }
728     }
729 }