added iOS source code
[wl-app.git] / iOS / Pods / Kingfisher / Sources / ImageCache.swift
diff --git a/iOS/Pods/Kingfisher/Sources/ImageCache.swift b/iOS/Pods/Kingfisher/Sources/ImageCache.swift
new file mode 100755 (executable)
index 0000000..a29fe82
--- /dev/null
@@ -0,0 +1,729 @@
+//
+//  ImageCache.swift
+//  Kingfisher
+//
+//  Created by Wei Wang on 15/4/6.
+//
+//  Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
+//
+//  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.
+
+#if os(macOS)
+import AppKit
+#else
+import UIKit
+#endif
+
+public extension Notification.Name {
+    /**
+     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.
+     
+     The `object` of this notification is the `ImageCache` object which sends the notification.
+     
+     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.
+     
+     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.
+     */
+    public static var KingfisherDidCleanDiskCache = Notification.Name.init("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache")
+}
+
+/**
+Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`.
+*/
+public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash"
+
+/// It represents a task of retrieving image. You can call `cancel` on it to stop the process.
+public typealias RetrieveImageDiskTask = DispatchWorkItem
+
+/**
+Cache type of a cached image.
+
+- None:   The image is not cached yet when retrieving it.
+- Memory: The image is cached in memory.
+- Disk:   The image is cached in disk.
+*/
+public enum CacheType {
+    case none, memory, disk
+    
+    public var cached: Bool {
+        switch self {
+        case .memory, .disk: return true
+        case .none: return false
+        }
+    }
+}
+
+/// `ImageCache` represents both the memory and disk cache system of Kingfisher. 
+/// While a default image cache object will be used if you prefer the extension methods of Kingfisher, 
+/// you can create your own cache object and configure it as your need. You could use an `ImageCache`
+/// object to manipulate memory and disk cache for Kingfisher.
+open class ImageCache {
+
+    //Memory
+    fileprivate let memoryCache = NSCache<NSString, AnyObject>()
+    
+    /// The largest cache cost of memory cache. The total cost is pixel count of 
+    /// all cached images in memory.
+    /// Default is unlimited. Memory cache will be purged automatically when a 
+    /// memory warning notification is received.
+    open var maxMemoryCost: UInt = 0 {
+        didSet {
+            self.memoryCache.totalCostLimit = Int(maxMemoryCost)
+        }
+    }
+    
+    //Disk
+    fileprivate let ioQueue: DispatchQueue
+    fileprivate var fileManager: FileManager!
+    
+    ///The disk cache location.
+    open let diskCachePath: String
+  
+    /// The default file extension appended to cached files.
+    open var pathExtension: String?
+    
+    /// The longest time duration in second of the cache being stored in disk. 
+    /// Default is 1 week (60 * 60 * 24 * 7 seconds).
+    /// Setting this to a negative value will make the disk cache never expiring.
+    open var maxCachePeriodInSecond: TimeInterval = 60 * 60 * 24 * 7 //Cache exists for 1 week
+    
+    /// The largest disk size can be taken for the cache. It is the total 
+    /// allocated size of cached files in bytes.
+    /// Default is no limit.
+    open var maxDiskCacheSize: UInt = 0
+    
+    fileprivate let processQueue: DispatchQueue
+    
+    /// The default cache.
+    public static let `default` = ImageCache(name: "default")
+    
+    /// Closure that defines the disk cache path from a given path and cacheName.
+    public typealias DiskCachePathClosure = (String?, String) -> String
+    
+    /// The default DiskCachePathClosure
+    public final class func defaultDiskCachePathClosure(path: String?, cacheName: String) -> String {
+        let dstPath = path ?? NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
+        return (dstPath as NSString).appendingPathComponent(cacheName)
+    }
+    
+    /**
+    Init method. Passing a name for the cache. It represents a cache folder in the memory and disk.
+    
+    - parameter name: Name of the cache. It will be used as the memory cache name and the disk cache folder name 
+                      appending to the cache path. This value should not be an empty string.
+    - parameter path: Optional - Location of cache path on disk. If `nil` is passed in (the default value),
+                      the `.cachesDirectory` in of your app will be used.
+    - parameter diskCachePathClosure: Closure that takes in an optional initial path string and generates
+                      the final disk cache path. You could use it to fully customize your cache path.
+    
+    - returns: The cache object.
+    */
+    public init(name: String,
+                path: String? = nil,
+                diskCachePathClosure: DiskCachePathClosure = ImageCache.defaultDiskCachePathClosure)
+    {
+        
+        if name.isEmpty {
+            fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
+        }
+        
+        let cacheName = "com.onevcat.Kingfisher.ImageCache.\(name)"
+        memoryCache.name = cacheName
+        
+        diskCachePath = diskCachePathClosure(path, cacheName)
+        
+        let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(name)"
+        ioQueue = DispatchQueue(label: ioQueueName)
+        
+        let processQueueName = "com.onevcat.Kingfisher.ImageCache.processQueue.\(name)"
+        processQueue = DispatchQueue(label: processQueueName, attributes: .concurrent)
+        
+        ioQueue.sync { fileManager = FileManager() }
+        
+#if !os(macOS) && !os(watchOS)
+        NotificationCenter.default.addObserver(
+            self, selector: #selector(clearMemoryCache), name: .UIApplicationDidReceiveMemoryWarning, object: nil)
+        NotificationCenter.default.addObserver(
+            self, selector: #selector(cleanExpiredDiskCache), name: .UIApplicationWillTerminate, object: nil)
+        NotificationCenter.default.addObserver(
+            self, selector: #selector(backgroundCleanExpiredDiskCache), name: .UIApplicationDidEnterBackground, object: nil)
+#endif
+    }
+    
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+    }
+
+
+    // MARK: - Store & Remove
+
+    /**
+    Store an image to cache. It will be saved to both memory and disk. It is an async operation.
+    
+    - parameter image:             The image to be stored.
+    - parameter original:          The original data of the image.
+                                   Kingfisher will use it to check the format of the image and optimize cache size on disk.
+                                   If `nil` is supplied, the image data will be saved as a normalized PNG file.
+                                   It is strongly suggested to supply it whenever possible, to get a better performance and disk usage.
+    - parameter key:               Key for the image.
+    - parameter identifier:        The identifier of processor used. If you are using a processor for the image, pass the identifier of
+                                   processor to it.
+                                   This identifier will be used to generate a corresponding key for the combination of `key` and processor.
+    - parameter toDisk:            Whether this image should be cached to disk or not. If false, the image will be only cached in memory.
+    - parameter completionHandler: Called when store operation completes.
+    */
+    open func store(_ image: Image,
+                      original: Data? = nil,
+                      forKey key: String,
+                      processorIdentifier identifier: String = "",
+                      cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
+                      toDisk: Bool = true,
+                      completionHandler: (() -> Void)? = nil)
+    {
+        
+        let computedKey = key.computedKey(with: identifier)
+        memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
+
+        func callHandlerInMainQueue() {
+            if let handler = completionHandler {
+                DispatchQueue.main.async {
+                    handler()
+                }
+            }
+        }
+        
+        if toDisk {
+            ioQueue.async {
+                
+                if let data = serializer.data(with: image, original: original) {
+                    if !self.fileManager.fileExists(atPath: self.diskCachePath) {
+                        do {
+                            try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
+                        } catch _ {}
+                    }
+                    
+                    self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
+                }
+                callHandlerInMainQueue()
+            }
+        } else {
+            callHandlerInMainQueue()
+        }
+    }
+    
+    /**
+    Remove the image for key for the cache. It will be opted out from both memory and disk. 
+    It is an async operation.
+    
+    - parameter key:               Key for the image.
+    - parameter identifier:        The identifier of processor used. If you are using a processor for the image, pass the identifier of processor to it.
+                                   This identifier will be used to generate a corresponding key for the combination of `key` and processor.
+    - parameter fromMemory:        Whether this image should be removed from memory or not. If false, the image won't be removed from memory.
+    - parameter fromDisk:          Whether this image should be removed from disk or not. If false, the image won't be removed from disk.
+    - parameter completionHandler: Called when removal operation completes.
+    */
+    open func removeImage(forKey key: String,
+                          processorIdentifier identifier: String = "",
+                          fromMemory: Bool = true,
+                          fromDisk: Bool = true,
+                          completionHandler: (() -> Void)? = nil)
+    {
+        let computedKey = key.computedKey(with: identifier)
+
+        if fromMemory {
+            memoryCache.removeObject(forKey: computedKey as NSString)
+        }
+        
+        func callHandlerInMainQueue() {
+            if let handler = completionHandler {
+                DispatchQueue.main.async {
+                    handler()
+                }
+            }
+        }
+        
+        if fromDisk {
+            ioQueue.async{
+                do {
+                    try self.fileManager.removeItem(atPath: self.cachePath(forComputedKey: computedKey))
+                } catch _ {}
+                callHandlerInMainQueue()
+            }
+        } else {
+            callHandlerInMainQueue()
+        }
+    }
+
+    // MARK: - Get data from cache
+
+    /**
+    Get an image for a key from memory or disk.
+    
+    - parameter key:               Key for the image.
+    - parameter options:           Options of retrieving image. If you need to retrieve an image which was 
+                                   stored with a specified `ImageProcessor`, pass the processor in the option too.
+    - parameter 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`.
+    
+    - returns: The retrieving task.
+    */
+    @discardableResult
+    open func retrieveImage(forKey key: String,
+                               options: KingfisherOptionsInfo?,
+                     completionHandler: ((Image?, CacheType) -> Void)?) -> RetrieveImageDiskTask?
+    {
+        // No completion handler. Not start working and early return.
+        guard let completionHandler = completionHandler else {
+            return nil
+        }
+        
+        var block: RetrieveImageDiskTask?
+        let options = options ?? KingfisherEmptyOptionsInfo
+        let imageModifier = options.imageModifier
+
+        if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
+            options.callbackDispatchQueue.safeAsync {
+                completionHandler(imageModifier.modify(image), .memory)
+            }
+        } else if options.fromMemoryCacheOrRefresh { // Only allows to get images from memory cache.
+            options.callbackDispatchQueue.safeAsync {
+                completionHandler(nil, .none)
+            }
+        } else {
+            var sSelf: ImageCache! = self
+            block = DispatchWorkItem(block: {
+                // Begin to load image from disk
+                if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options) {
+                    if options.backgroundDecode {
+                        sSelf.processQueue.async {
+
+                            let result = image.kf.decoded
+                            
+                            sSelf.store(result,
+                                        forKey: key,
+                                        processorIdentifier: options.processor.identifier,
+                                        cacheSerializer: options.cacheSerializer,
+                                        toDisk: false,
+                                        completionHandler: nil)
+                            options.callbackDispatchQueue.safeAsync {
+                                completionHandler(imageModifier.modify(result), .memory)
+                                sSelf = nil
+                            }
+                        }
+                    } else {
+                        sSelf.store(image,
+                                    forKey: key,
+                                    processorIdentifier: options.processor.identifier,
+                                    cacheSerializer: options.cacheSerializer,
+                                    toDisk: false,
+                                    completionHandler: nil
+                        )
+                        options.callbackDispatchQueue.safeAsync {
+                            completionHandler(imageModifier.modify(image), .disk)
+                            sSelf = nil
+                        }
+                    }
+                } else {
+                    // No image found from either memory or disk
+                    options.callbackDispatchQueue.safeAsync {
+                        completionHandler(nil, .none)
+                        sSelf = nil
+                    }
+                }
+            })
+            
+            sSelf.ioQueue.async(execute: block!)
+        }
+    
+        return block
+    }
+    
+    /**
+    Get an image for a key from memory.
+    
+    - parameter key:     Key for the image.
+    - parameter options: Options of retrieving image. If you need to retrieve an image which was 
+                         stored with a specified `ImageProcessor`, pass the processor in the option too.
+    - returns: The image object if it is cached, or `nil` if there is no such key in the cache.
+    */
+    open func retrieveImageInMemoryCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
+        
+        let options = options ?? KingfisherEmptyOptionsInfo
+        let computedKey = key.computedKey(with: options.processor.identifier)
+        
+        return memoryCache.object(forKey: computedKey as NSString) as? Image
+    }
+    
+    /**
+    Get an image for a key from disk.
+    
+    - parameter key:     Key for the image.
+    - parameter options: Options of retrieving image. If you need to retrieve an image which was
+                         stored with a specified `ImageProcessor`, pass the processor in the option too.
+
+    - returns: The image object if it is cached, or `nil` if there is no such key in the cache.
+    */
+    open func retrieveImageInDiskCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? {
+        
+        let options = options ?? KingfisherEmptyOptionsInfo
+        let computedKey = key.computedKey(with: options.processor.identifier)
+        
+        return diskImage(forComputedKey: computedKey, serializer: options.cacheSerializer, options: options)
+    }
+
+
+    // MARK: - Clear & Clean
+
+    /**
+    Clear memory cache.
+    */
+    @objc public func clearMemoryCache() {
+        memoryCache.removeAllObjects()
+    }
+    
+    /**
+    Clear disk cache. This is an async operation.
+    
+    - parameter completionHander: Called after the operation completes.
+    */
+    open func clearDiskCache(completion handler: (()->())? = nil) {
+        ioQueue.async {
+            do {
+                try self.fileManager.removeItem(atPath: self.diskCachePath)
+                try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
+            } catch _ { }
+            
+            if let handler = handler {
+                DispatchQueue.main.async {
+                    handler()
+                }
+            }
+        }
+    }
+    
+    /**
+    Clean expired disk cache. This is an async operation.
+    */
+    @objc fileprivate func cleanExpiredDiskCache() {
+        cleanExpiredDiskCache(completion: nil)
+    }
+    
+    /**
+    Clean expired disk cache. This is an async operation.
+    
+    - parameter completionHandler: Called after the operation completes.
+    */
+    open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {
+        
+        // Do things in cocurrent io queue
+        ioQueue.async {
+            
+            var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
+            
+            for fileURL in URLsToDelete {
+                do {
+                    try self.fileManager.removeItem(at: fileURL)
+                } catch _ { }
+            }
+                
+            if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
+                let targetSize = self.maxDiskCacheSize / 2
+                    
+                // Sort files by last modify date. We want to clean from the oldest files.
+                let sortedFiles = cachedFiles.keysSortedByValue {
+                    resourceValue1, resourceValue2 -> Bool in
+                    
+                    if let date1 = resourceValue1.contentAccessDate,
+                       let date2 = resourceValue2.contentAccessDate
+                    {
+                        return date1.compare(date2) == .orderedAscending
+                    }
+                    
+                    // Not valid date information. This should not happen. Just in case.
+                    return true
+                }
+                
+                for fileURL in sortedFiles {
+                    
+                    do {
+                        try self.fileManager.removeItem(at: fileURL)
+                    } catch { }
+                        
+                    URLsToDelete.append(fileURL)
+                    
+                    if let fileSize = cachedFiles[fileURL]?.totalFileAllocatedSize {
+                        diskCacheSize -= UInt(fileSize)
+                    }
+                    
+                    if diskCacheSize < targetSize {
+                        break
+                    }
+                }
+            }
+                
+            DispatchQueue.main.async {
+                
+                if URLsToDelete.count != 0 {
+                    let cleanedHashes = URLsToDelete.map { $0.lastPathComponent }
+                    NotificationCenter.default.post(name: .KingfisherDidCleanDiskCache, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
+                }
+                
+                handler?()
+            }
+        }
+    }
+    
+    fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
+        
+        let diskCacheURL = URL(fileURLWithPath: diskCachePath)
+        let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
+        let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
+        
+        var cachedFiles = [URL: URLResourceValues]()
+        var urlsToDelete = [URL]()
+        var diskCacheSize: UInt = 0
+
+        for fileUrl in (try? fileManager.contentsOfDirectory(at: diskCacheURL, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)) ?? [] {
+
+            do {
+                let resourceValues = try fileUrl.resourceValues(forKeys: resourceKeys)
+                // If it is a Directory. Continue to next file URL.
+                if resourceValues.isDirectory == true {
+                    continue
+                }
+
+                // If this file is expired, add it to URLsToDelete
+                if !onlyForCacheSize,
+                    let expiredDate = expiredDate,
+                    let lastAccessData = resourceValues.contentAccessDate,
+                    (lastAccessData as NSDate).laterDate(expiredDate) == expiredDate
+                {
+                    urlsToDelete.append(fileUrl)
+                    continue
+                }
+
+                if let fileSize = resourceValues.totalFileAllocatedSize {
+                    diskCacheSize += UInt(fileSize)
+                    if !onlyForCacheSize {
+                        cachedFiles[fileUrl] = resourceValues
+                    }
+                }
+            } catch _ { }
+        }
+
+        return (urlsToDelete, diskCacheSize, cachedFiles)
+    }
+
+#if !os(macOS) && !os(watchOS)
+    /**
+    Clean expired disk cache when app in background. This is an async operation.
+    In most cases, you should not call this method explicitly. 
+    It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received.
+    */
+    @objc public func backgroundCleanExpiredDiskCache() {
+        // if 'sharedApplication()' is unavailable, then return
+        guard let sharedApplication = Kingfisher<UIApplication>.shared else { return }
+
+        func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) {
+            sharedApplication.endBackgroundTask(task)
+            task = UIBackgroundTaskInvalid
+        }
+        
+        var backgroundTask: UIBackgroundTaskIdentifier!
+        backgroundTask = sharedApplication.beginBackgroundTask {
+            endBackgroundTask(&backgroundTask!)
+        }
+        
+        cleanExpiredDiskCache {
+            endBackgroundTask(&backgroundTask!)
+        }
+    }
+#endif
+
+
+    // MARK: - Check cache status
+    
+    /// Cache type for checking whether an image is cached for a key in current cache.
+    ///
+    /// - Parameters:
+    ///   - key: Key for the image.
+    ///   - identifier: Processor identifier which used for this image. Default is empty string.
+    /// - Returns: A `CacheType` instance which indicates the cache status. `.none` means the image is not in cache yet.
+    open func imageCachedType(forKey key: String, processorIdentifier identifier: String = "") -> CacheType {
+        let computedKey = key.computedKey(with: identifier)
+        
+        if memoryCache.object(forKey: computedKey as NSString) != nil {
+            return .memory
+        }
+        
+        let filePath = cachePath(forComputedKey: computedKey)
+        
+        var diskCached = false
+        ioQueue.sync {
+            diskCached = fileManager.fileExists(atPath: filePath)
+        }
+        
+        if diskCached {
+            return .disk
+        }
+        
+        return .none
+    }
+    
+    /**
+    Get the hash for the key. This could be used for matching files.
+    
+    - parameter key:        The key which is used for caching.
+    - parameter identifier: The identifier of processor used. If you are using a processor for the image, pass the identifier of processor to it.
+    
+     - returns: Corresponding hash.
+    */
+    open func hash(forKey key: String, processorIdentifier identifier: String = "") -> String {
+        let computedKey = key.computedKey(with: identifier)
+        return cacheFileName(forComputedKey: computedKey)
+    }
+    
+    /**
+    Calculate the disk size taken by cache. 
+    It is the total allocated size of the cached files in bytes.
+    
+    - parameter completionHandler: Called with the calculated size when finishes.
+    */
+    open func calculateDiskCacheSize(completion handler: @escaping ((_ size: UInt) -> Void)) {
+        ioQueue.async {
+            let (_, diskCacheSize, _) = self.travelCachedFiles(onlyForCacheSize: true)
+            DispatchQueue.main.async {
+                handler(diskCacheSize)
+            }
+        }
+    }
+    
+    /**
+    Get the cache path for the key.
+    It is useful for projects with UIWebView or anyone that needs access to the local file path.
+    
+    i.e. Replace the `<img src='path_for_key'>` tag in your HTML.
+     
+    - Note: This method does not guarantee there is an image already cached in the path. It just returns the path
+      that the image should be.
+      You could use `isImageCached(forKey:)` method to check whether the image is cached under that key.
+    */
+    open func cachePath(forKey key: String, processorIdentifier identifier: String = "") -> String {
+        let computedKey = key.computedKey(with: identifier)
+        return cachePath(forComputedKey: computedKey)
+    }
+
+    open func cachePath(forComputedKey key: String) -> String {
+        let fileName = cacheFileName(forComputedKey: key)
+        return (diskCachePath as NSString).appendingPathComponent(fileName)
+    }
+}
+
+// MARK: - Internal Helper
+extension ImageCache {
+  
+    func diskImage(forComputedKey key: String, serializer: CacheSerializer, options: KingfisherOptionsInfo) -> Image? {
+        if let data = diskImageData(forComputedKey: key) {
+            return serializer.image(with: data, options: options)
+        } else {
+            return nil
+        }
+    }
+    
+    func diskImageData(forComputedKey key: String) -> Data? {
+        let filePath = cachePath(forComputedKey: key)
+        return (try? Data(contentsOf: URL(fileURLWithPath: filePath)))
+    }
+    
+    func cacheFileName(forComputedKey key: String) -> String {
+        if let ext = self.pathExtension {
+          return (key.kf.md5 as NSString).appendingPathExtension(ext)!
+        }
+        return key.kf.md5
+    }
+}
+
+// MARK: - Deprecated
+extension ImageCache {
+    /**
+     *  Cache result for checking whether an image is cached for a key.
+     */
+    @available(*, deprecated,
+    message: "CacheCheckResult is deprecated. Use imageCachedType(forKey:processorIdentifier:) API instead.")
+    public struct CacheCheckResult {
+        public let cached: Bool
+        public let cacheType: CacheType?
+    }
+    
+    /**
+     Check whether an image is cached for a key.
+     
+     - parameter key: Key for the image.
+     
+     - returns: The check result.
+     */
+    @available(*, deprecated,
+    message: "Use imageCachedType(forKey:processorIdentifier:) instead. CacheCheckResult.none indicates not being cached.",
+    renamed: "imageCachedType(forKey:processorIdentifier:)")
+    open func isImageCached(forKey key: String, processorIdentifier identifier: String = "") -> CacheCheckResult {
+        let result = imageCachedType(forKey: key, processorIdentifier: identifier)
+        switch result {
+        case .memory, .disk:
+            return CacheCheckResult(cached: true, cacheType: result)
+        case .none:
+            return CacheCheckResult(cached: false, cacheType: nil)
+        }
+    }
+}
+
+extension Kingfisher where Base: Image {
+    var imageCost: Int {
+        return images == nil ?
+            Int(size.height * size.width * scale * scale) :
+            Int(size.height * size.width * scale * scale) * images!.count
+    }
+}
+
+extension Dictionary {
+    func keysSortedByValue(_ isOrderedBefore: (Value, Value) -> Bool) -> [Key] {
+        return Array(self).sorted{ isOrderedBefore($0.1, $1.1) }.map{ $0.0 }
+    }
+}
+
+#if !os(macOS) && !os(watchOS)
+// MARK: - For App Extensions
+extension UIApplication: KingfisherCompatible { }
+extension Kingfisher where Base: UIApplication {
+    public static var shared: UIApplication? {
+        let selector = NSSelectorFromString("sharedApplication")
+        guard Base.responds(to: selector) else { return nil }
+        return Base.perform(selector).takeUnretainedValue() as? UIApplication
+    }
+}
+#endif
+
+extension String {
+    func computedKey(with identifier: String) -> String {
+        if identifier.isEmpty {
+            return self
+        } else {
+            return appending("@\(identifier)")
+        }
+    }
+}