You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
743 lines
30 KiB
743 lines
30 KiB
![]()
2 years ago
|
//
|
||
|
// 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
|
||
|
|
||
|
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 let 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.
|
||
|
public 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.
|
||
|
*/
|
||
|
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)
|
||
|
|
||
|
#if swift(>=4.2)
|
||
|
let memoryNotification = UIApplication.didReceiveMemoryWarningNotification
|
||
|
let terminateNotification = UIApplication.willTerminateNotification
|
||
|
let enterbackgroundNotification = UIApplication.didEnterBackgroundNotification
|
||
|
#else
|
||
|
let memoryNotification = NSNotification.Name.UIApplicationDidReceiveMemoryWarning
|
||
|
let terminateNotification = NSNotification.Name.UIApplicationWillTerminate
|
||
|
let enterbackgroundNotification = NSNotification.Name.UIApplicationDidEnterBackground
|
||
|
#endif
|
||
|
|
||
|
NotificationCenter.default.addObserver(
|
||
|
self, selector: #selector(clearMemoryCache), name: memoryNotification, object: nil)
|
||
|
NotificationCenter.default.addObserver(
|
||
|
self, selector: #selector(cleanExpiredDiskCache), name: terminateNotification, object: nil)
|
||
|
NotificationCenter.default.addObserver(
|
||
|
self, selector: #selector(backgroundCleanExpiredDiskCache), name: enterbackgroundNotification, 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), .disk)
|
||
|
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 concurrent 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)
|
||
|
#if swift(>=4.2)
|
||
|
task = UIBackgroundTaskIdentifier.invalid
|
||
|
#else
|
||
|
task = UIBackgroundTaskInvalid
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
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)")
|
||
|
}
|
||
|
}
|
||
|
}
|