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.
843 lines
36 KiB
843 lines
36 KiB
//
|
|
// ImageCache.swift
|
|
// Kingfisher
|
|
//
|
|
// Created by Wei Wang on 15/4/6.
|
|
//
|
|
// Copyright (c) 2019 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.
|
|
public static let KingfisherDidCleanDiskCache =
|
|
Notification.Name("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache")
|
|
}
|
|
|
|
/// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`.
|
|
public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash"
|
|
|
|
/// 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 {
|
|
/// The image is not cached yet when retrieving it.
|
|
case none
|
|
/// The image is cached in memory.
|
|
case memory
|
|
/// The image is cached in disk.
|
|
case disk
|
|
|
|
/// Whether the cache type represents the image is already cached or not.
|
|
public var cached: Bool {
|
|
switch self {
|
|
case .memory, .disk: return true
|
|
case .none: return false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents the caching operation result.
|
|
public struct CacheStoreResult {
|
|
|
|
/// The cache result for memory cache. Caching an image to memory will never fail.
|
|
public let memoryCacheResult: Result<(), Never>
|
|
|
|
/// The cache result for disk cache. If an error happens during caching operation,
|
|
/// you can get it from `.failure` case of this `diskCacheResult`.
|
|
public let diskCacheResult: Result<(), KingfisherError>
|
|
}
|
|
|
|
extension Image: CacheCostCalculable {
|
|
/// Cost of an image
|
|
public var cacheCost: Int { return kf.cost }
|
|
}
|
|
|
|
extension Data: DataTransformable {
|
|
public func toData() throws -> Data {
|
|
return self
|
|
}
|
|
|
|
public static func fromData(_ data: Data) throws -> Data {
|
|
return data
|
|
}
|
|
|
|
public static let empty = Data()
|
|
}
|
|
|
|
|
|
/// Represents the getting image operation from the cache.
|
|
///
|
|
/// - disk: The image can be retrieved from disk cache.
|
|
/// - memory: The image can be retrieved memory cache.
|
|
/// - none: The image does not exist in the cache.
|
|
public enum ImageCacheResult {
|
|
|
|
/// The image can be retrieved from disk cache.
|
|
case disk(Image)
|
|
|
|
/// The image can be retrieved memory cache.
|
|
case memory(Image)
|
|
|
|
/// The image does not exist in the cache.
|
|
case none
|
|
|
|
/// Extracts the image from cache result. It returns the associated `Image` value for
|
|
/// `.disk` and `.memory` case. For `.none` case, `nil` is returned.
|
|
public var image: Image? {
|
|
switch self {
|
|
case .disk(let image): return image
|
|
case .memory(let image): return image
|
|
case .none: return nil
|
|
}
|
|
}
|
|
|
|
/// Returns the corresponding `CacheType` value based on the result type of `self`.
|
|
public var cacheType: CacheType {
|
|
switch self {
|
|
case .disk: return .disk
|
|
case .memory: return .memory
|
|
case .none: return .none
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a hybrid caching system which is composed by a `MemoryStorage.Backend` and a `DiskStorage.Backend`.
|
|
/// `ImageCache` is a high level abstract for storing an image as well as its data to disk memory and disk, and
|
|
/// retrieving them back.
|
|
///
|
|
/// 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 its storages as your need. This class also provide an interface for you to set
|
|
/// the memory and disk storage config.
|
|
open class ImageCache {
|
|
|
|
// MARK: Singleton
|
|
/// The default `ImageCache` object. Kingfisher will use this cache for its related methods if there is no
|
|
/// other cache specified. The `name` of this default cache is "default", and you should not use this name
|
|
/// for any of your customize cache.
|
|
public static let `default` = ImageCache(name: "default")
|
|
|
|
// MARK: Public Properties
|
|
/// The `MemoryStorage.Backend` object used in this cache. This storage holds loaded images in memory with a
|
|
/// reasonable expire duration and a maximum memory usage. To modify the configuration of a storage, just set
|
|
/// the storage `config` and its properties.
|
|
public let memoryStorage: MemoryStorage.Backend<Image>
|
|
|
|
/// The `DiskStorage.Backend` object used in this cache. This storage stores loaded images in disk with a
|
|
/// reasonable expire duration and a maximum disk usage. To modify the configuration of a storage, just set
|
|
/// the storage `config` and its properties.
|
|
public let diskStorage: DiskStorage.Backend<Data>
|
|
|
|
private let ioQueue: DispatchQueue
|
|
|
|
/// Closure that defines the disk cache path from a given path and cacheName.
|
|
public typealias DiskCachePathClosure = (URL, String) -> URL
|
|
|
|
// MARK: Initializers
|
|
|
|
/// Creates an `ImageCache` from a customized `MemoryStorage` and `DiskStorage`.
|
|
///
|
|
/// - Parameters:
|
|
/// - memoryStorage: The `MemoryStorage.Backend` object to use in the image cache.
|
|
/// - diskStorage: The `DiskStorage.Backend` object to use in the image cache.
|
|
public init(
|
|
memoryStorage: MemoryStorage.Backend<Image>,
|
|
diskStorage: DiskStorage.Backend<Data>)
|
|
{
|
|
self.memoryStorage = memoryStorage
|
|
self.diskStorage = diskStorage
|
|
let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(UUID().uuidString)"
|
|
ioQueue = DispatchQueue(label: ioQueueName)
|
|
|
|
let notifications: [(Notification.Name, Selector)]
|
|
#if !os(macOS) && !os(watchOS)
|
|
#if swift(>=4.2)
|
|
notifications = [
|
|
(UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)),
|
|
(UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)),
|
|
(UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache))
|
|
]
|
|
#else
|
|
notifications = [
|
|
(NSNotification.Name.UIApplicationDidReceiveMemoryWarning, #selector(clearMemoryCache)),
|
|
(NSNotification.Name.UIApplicationWillTerminate, #selector(cleanExpiredDiskCache)),
|
|
(NSNotification.Name.UIApplicationDidEnterBackground, #selector(backgroundCleanExpiredDiskCache))
|
|
]
|
|
#endif
|
|
#elseif os(macOS)
|
|
notifications = [
|
|
(NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)),
|
|
]
|
|
#else
|
|
notifications = []
|
|
#endif
|
|
notifications.forEach {
|
|
NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil)
|
|
}
|
|
}
|
|
|
|
/// Creates an `ImageCache` with a given `name`. Both `MemoryStorage` and `DiskStorage` will be created
|
|
/// with a default config based on the `name`.
|
|
///
|
|
/// - Parameter name: The name of cache object. It is used to setup disk cache directories and IO queue.
|
|
/// You should not use the same `name` for different caches, otherwise, the disk storage would
|
|
/// be conflicting to each other. The `name` should not be an empty string.
|
|
public convenience init(name: String) {
|
|
try! self.init(name: name, cacheDirectoryURL: nil, diskCachePathClosure: nil)
|
|
}
|
|
|
|
/// Creates an `ImageCache` with a given `name`, cache directory `path`
|
|
/// and a closure to modify the cache directory.
|
|
///
|
|
/// - Parameters:
|
|
/// - name: The name of cache object. It is used to setup disk cache directories and IO queue.
|
|
/// You should not use the same `name` for different caches, otherwise, the disk storage would
|
|
/// be conflicting to each other.
|
|
/// - cacheDirectoryURL: Location of cache directory URL on disk. It will be internally pass to the
|
|
/// initializer of `DiskStorage` as the disk cache directory. If `nil`, the cache
|
|
/// directory under user domain mask will be used.
|
|
/// - 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.
|
|
/// - Throws: An error that happens during image cache creating, such as unable to create a directory at the given
|
|
/// path.
|
|
public convenience init(
|
|
name: String,
|
|
cacheDirectoryURL: URL?,
|
|
diskCachePathClosure: DiskCachePathClosure? = nil) throws
|
|
{
|
|
if name.isEmpty {
|
|
fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
|
|
}
|
|
|
|
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
|
let costLimit = totalMemory / 4
|
|
let memoryStorage = MemoryStorage.Backend<Image>(config:
|
|
.init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit)))
|
|
|
|
var diskConfig = DiskStorage.Config(
|
|
name: name,
|
|
sizeLimit: 0,
|
|
directory: cacheDirectoryURL
|
|
)
|
|
if let closure = diskCachePathClosure {
|
|
diskConfig.cachePathBlock = closure
|
|
}
|
|
let diskStorage = try DiskStorage.Backend<Data>(config: diskConfig)
|
|
diskConfig.cachePathBlock = nil
|
|
|
|
self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: Storing Images
|
|
|
|
open func store(_ image: Image,
|
|
original: Data? = nil,
|
|
forKey key: String,
|
|
options: KingfisherParsedOptionsInfo,
|
|
toDisk: Bool = true,
|
|
completionHandler: ((CacheStoreResult) -> Void)? = nil)
|
|
{
|
|
let identifier = options.processor.identifier
|
|
let callbackQueue = options.callbackQueue
|
|
|
|
let computedKey = key.computedKey(with: identifier)
|
|
// Memory storage should not throw.
|
|
memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)
|
|
|
|
guard toDisk else {
|
|
if let completionHandler = completionHandler {
|
|
let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
|
|
callbackQueue.execute { completionHandler(result) }
|
|
}
|
|
return
|
|
}
|
|
|
|
ioQueue.async {
|
|
let serializer = options.cacheSerializer
|
|
if let data = serializer.data(with: image, original: original) {
|
|
self.syncStoreToDisk(
|
|
data,
|
|
forKey: key,
|
|
processorIdentifier: identifier,
|
|
callbackQueue: callbackQueue,
|
|
expiration: options.diskCacheExpiration,
|
|
completionHandler: completionHandler)
|
|
} else {
|
|
guard let completionHandler = completionHandler else { return }
|
|
|
|
let diskError = KingfisherError.cacheError(
|
|
reason: .cannotSerializeImage(image: image, original: original, serializer: serializer))
|
|
let result = CacheStoreResult(
|
|
memoryCacheResult: .success(()),
|
|
diskCacheResult: .failure(diskError))
|
|
callbackQueue.execute { completionHandler(result) }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stores an image to the cache.
|
|
///
|
|
/// - Parameters:
|
|
/// - image: The image to be stored.
|
|
/// - original: The original data of the image. This value will be forwarded to the provided `serializer` for
|
|
/// further use. By default, Kingfisher uses a `DefaultCacheSerializer` to serialize the image to
|
|
/// data for caching in disk, it checks the image format based on `original` data to determine in
|
|
/// which image format should be used. For other types of `serializer`, it depends on their
|
|
/// implementation detail on how to use this original data.
|
|
/// - key: The key used for caching the image.
|
|
/// - identifier: The identifier of processor being used for caching. If you are using a processor for the
|
|
/// image, pass the identifier of processor to this parameter.
|
|
/// - serializer: The `CacheSerializer`
|
|
/// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory.
|
|
/// Otherwise, it is cached in both memory storage and disk storage. Default is `true`.
|
|
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. For case
|
|
/// that `toDisk` is `false`, a `.untouch` queue means `callbackQueue` will be invoked from the
|
|
/// caller queue of this method. If `toDisk` is `true`, the `completionHandler` will be called
|
|
/// from an internal file IO queue. To change this behavior, specify another `CallbackQueue`
|
|
/// value.
|
|
/// - completionHandler: A closure which is invoked when the cache operation finishes.
|
|
open func store(_ image: Image,
|
|
original: Data? = nil,
|
|
forKey key: String,
|
|
processorIdentifier identifier: String = "",
|
|
cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
|
|
toDisk: Bool = true,
|
|
callbackQueue: CallbackQueue = .untouch,
|
|
completionHandler: ((CacheStoreResult) -> Void)? = nil)
|
|
{
|
|
struct TempProcessor: ImageProcessor {
|
|
let identifier: String
|
|
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
let options = KingfisherParsedOptionsInfo([
|
|
.processor(TempProcessor(identifier: identifier)),
|
|
.cacheSerializer(serializer),
|
|
.callbackQueue(callbackQueue)
|
|
])
|
|
store(image, original: original, forKey: key, options: options,
|
|
toDisk: toDisk, completionHandler: completionHandler)
|
|
}
|
|
|
|
open func storeToDisk(
|
|
_ data: Data,
|
|
forKey key: String,
|
|
processorIdentifier identifier: String = "",
|
|
expiration: StorageExpiration? = nil,
|
|
callbackQueue: CallbackQueue = .untouch,
|
|
completionHandler: ((CacheStoreResult) -> Void)? = nil)
|
|
{
|
|
ioQueue.async {
|
|
self.syncStoreToDisk(
|
|
data,
|
|
forKey: key,
|
|
processorIdentifier: identifier,
|
|
callbackQueue: callbackQueue,
|
|
expiration: expiration,
|
|
completionHandler: completionHandler)
|
|
}
|
|
}
|
|
|
|
private func syncStoreToDisk(
|
|
_ data: Data,
|
|
forKey key: String,
|
|
processorIdentifier identifier: String = "",
|
|
callbackQueue: CallbackQueue = .untouch,
|
|
expiration: StorageExpiration? = nil,
|
|
completionHandler: ((CacheStoreResult) -> Void)? = nil)
|
|
{
|
|
let computedKey = key.computedKey(with: identifier)
|
|
let result: CacheStoreResult
|
|
do {
|
|
try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration)
|
|
result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
|
|
} catch {
|
|
let diskError: KingfisherError
|
|
if let error = error as? KingfisherError {
|
|
diskError = error
|
|
} else {
|
|
diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error))
|
|
}
|
|
|
|
result = CacheStoreResult(
|
|
memoryCacheResult: .success(()),
|
|
diskCacheResult: .failure(diskError)
|
|
)
|
|
}
|
|
if let completionHandler = completionHandler {
|
|
callbackQueue.execute { completionHandler(result) }
|
|
}
|
|
}
|
|
|
|
// MARK: Removing Images
|
|
|
|
/// Removes the image for the given key from the cache.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: The key used for caching the image.
|
|
/// - identifier: The identifier of processor being used for caching. If you are using a processor for the
|
|
/// image, pass the identifier of processor to this parameter.
|
|
/// - fromMemory: Whether this image should be removed from memory storage or not.
|
|
/// If `false`, the image won't be removed from the memory storage. Default is `true`.
|
|
/// - fromDisk: Whether this image should be removed from disk storage or not.
|
|
/// If `false`, the image won't be removed from the disk storage. Default is `true`.
|
|
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`.
|
|
/// - completionHandler: A closure which is invoked when the cache removing operation finishes.
|
|
open func removeImage(forKey key: String,
|
|
processorIdentifier identifier: String = "",
|
|
fromMemory: Bool = true,
|
|
fromDisk: Bool = true,
|
|
callbackQueue: CallbackQueue = .untouch,
|
|
completionHandler: (() -> Void)? = nil)
|
|
{
|
|
let computedKey = key.computedKey(with: identifier)
|
|
|
|
if fromMemory {
|
|
try? memoryStorage.remove(forKey: computedKey)
|
|
}
|
|
|
|
if fromDisk {
|
|
ioQueue.async{
|
|
try? self.diskStorage.remove(forKey: computedKey)
|
|
if let completionHandler = completionHandler {
|
|
callbackQueue.execute { completionHandler() }
|
|
}
|
|
}
|
|
} else {
|
|
if let completionHandler = completionHandler {
|
|
callbackQueue.execute { completionHandler() }
|
|
}
|
|
}
|
|
}
|
|
|
|
func retrieveImage(forKey key: String,
|
|
options: KingfisherParsedOptionsInfo,
|
|
callbackQueue: CallbackQueue = .untouch,
|
|
completionHandler: ((Result<ImageCacheResult, KingfisherError>) -> Void)?)
|
|
{
|
|
// No completion handler. No need to start working and early return.
|
|
guard let completionHandler = completionHandler else { return }
|
|
|
|
// Try to check the image from memory cache first.
|
|
if let image = retrieveImageInMemoryCache(forKey: key, options: options) {
|
|
let image = options.imageModifier?.modify(image) ?? image
|
|
callbackQueue.execute { completionHandler(.success(.memory(image))) }
|
|
} else if options.fromMemoryCacheOrRefresh {
|
|
callbackQueue.execute { completionHandler(.success(.none)) }
|
|
} else {
|
|
// Begin to disk search.
|
|
self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) {
|
|
result in
|
|
// The callback queue is already correct in this closure.
|
|
switch result {
|
|
case .success(let image):
|
|
|
|
guard let image = image else {
|
|
// No image found in disk storage.
|
|
completionHandler(.success(.none))
|
|
return
|
|
}
|
|
|
|
let finalImage = options.imageModifier?.modify(image) ?? image
|
|
// Cache the disk image to memory.
|
|
// We are passing `false` to `toDisk`, the memory cache does not change
|
|
// callback queue, we can call `completionHandler` without another dispatch.
|
|
var cacheOptions = options
|
|
cacheOptions.callbackQueue = .untouch
|
|
self.store(
|
|
finalImage,
|
|
forKey: key,
|
|
options: cacheOptions,
|
|
toDisk: false)
|
|
{
|
|
_ in
|
|
completionHandler(.success(.disk(finalImage)))
|
|
}
|
|
case .failure(let error):
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Getting Images
|
|
|
|
/// Gets an image for a given key from the cache, either from memory storage or disk storage.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: The key used for caching the image.
|
|
/// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
|
|
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`.
|
|
/// - completionHandler: A closure which is invoked when the image getting operation finishes. If the
|
|
/// image retrieving operation finishes without problem, an `ImageCacheResult` value
|
|
/// will be sent to this closure as result. Otherwise, a `KingfisherError` result
|
|
/// with detail failing reason will be sent.
|
|
open func retrieveImage(forKey key: String,
|
|
options: KingfisherOptionsInfo? = nil,
|
|
callbackQueue: CallbackQueue = .untouch,
|
|
completionHandler: ((Result<ImageCacheResult, KingfisherError>) -> Void)?)
|
|
{
|
|
retrieveImage(
|
|
forKey: key,
|
|
options: KingfisherParsedOptionsInfo(options),
|
|
callbackQueue: callbackQueue,
|
|
completionHandler: completionHandler)
|
|
}
|
|
|
|
func retrieveImageInMemoryCache(
|
|
forKey key: String,
|
|
options: KingfisherParsedOptionsInfo) -> Image?
|
|
{
|
|
let computedKey = key.computedKey(with: options.processor.identifier)
|
|
do {
|
|
return try memoryStorage.value(forKey: computedKey)
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Gets an image for a given key from the memory storage.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: The key used for caching the image.
|
|
/// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
|
|
/// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or
|
|
/// has already expired, `nil` is returned.
|
|
open func retrieveImageInMemoryCache(
|
|
forKey key: String,
|
|
options: KingfisherOptionsInfo? = nil) -> Image?
|
|
{
|
|
return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options))
|
|
}
|
|
|
|
func retrieveImageInDiskCache(
|
|
forKey key: String,
|
|
options: KingfisherParsedOptionsInfo,
|
|
callbackQueue: CallbackQueue = .untouch,
|
|
completionHandler: @escaping (Result<Image?, KingfisherError>) -> Void)
|
|
{
|
|
let computedKey = key.computedKey(with: options.processor.identifier)
|
|
let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue)
|
|
loadingQueue.execute {
|
|
do {
|
|
var image: Image? = nil
|
|
if let data = try self.diskStorage.value(forKey: computedKey) {
|
|
image = options.cacheSerializer.image(with: data, options: options)
|
|
}
|
|
callbackQueue.execute { completionHandler(.success(image)) }
|
|
} catch {
|
|
if let error = error as? KingfisherError {
|
|
callbackQueue.execute { completionHandler(.failure(error)) }
|
|
} else {
|
|
assertionFailure("The internal thrown error should be a `KingfisherError`.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Gets an image for a given key from the disk storage.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: The key used for caching the image.
|
|
/// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
|
|
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`.
|
|
/// - completionHandler: A closure which is invoked when the operation finishes.
|
|
open func retrieveImageInDiskCache(
|
|
forKey key: String,
|
|
options: KingfisherOptionsInfo? = nil,
|
|
callbackQueue: CallbackQueue = .untouch,
|
|
completionHandler: @escaping (Result<Image?, KingfisherError>) -> Void)
|
|
{
|
|
retrieveImageInDiskCache(
|
|
forKey: key,
|
|
options: KingfisherParsedOptionsInfo(options),
|
|
callbackQueue: callbackQueue,
|
|
completionHandler: completionHandler)
|
|
}
|
|
|
|
// MARK: Cleaning
|
|
/// Clears the memory storage of this cache.
|
|
@objc public func clearMemoryCache() {
|
|
try? memoryStorage.removeAll()
|
|
}
|
|
|
|
/// Clears the disk storage of this cache. This is an async operation.
|
|
///
|
|
/// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.
|
|
/// This `handler` will be called from the main queue.
|
|
open func clearDiskCache(completion handler: (()->())? = nil) {
|
|
ioQueue.async {
|
|
do {
|
|
try self.diskStorage.removeAll()
|
|
} catch _ { }
|
|
if let handler = handler {
|
|
DispatchQueue.main.async { handler() }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clears the expired images from disk storage. This is an async operation.
|
|
open func cleanExpiredMemoryCache() {
|
|
memoryStorage.removeExpired()
|
|
}
|
|
|
|
/// Clears the expired images from disk storage. This is an async operation.
|
|
@objc func cleanExpiredDiskCache() {
|
|
cleanExpiredDiskCache(completion: nil)
|
|
}
|
|
|
|
/// Clears the expired images from disk storage. This is an async operation.
|
|
///
|
|
/// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.
|
|
/// This `handler` will be called from the main queue.
|
|
open func cleanExpiredDiskCache(completion handler: (() -> Void)? = nil) {
|
|
ioQueue.async {
|
|
do {
|
|
var removed: [URL] = []
|
|
let removedExpired = try self.diskStorage.removeExpiredValues()
|
|
removed.append(contentsOf: removedExpired)
|
|
|
|
let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues()
|
|
removed.append(contentsOf: removedSizeExceeded)
|
|
|
|
if !removed.isEmpty {
|
|
DispatchQueue.main.async {
|
|
let cleanedHashes = removed.map { $0.lastPathComponent }
|
|
NotificationCenter.default.post(
|
|
name: .KingfisherDidCleanDiskCache,
|
|
object: self,
|
|
userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
|
|
}
|
|
}
|
|
|
|
if let handler = handler {
|
|
DispatchQueue.main.async { handler() }
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
#if !os(macOS) && !os(watchOS)
|
|
/// Clears the expired images from disk storage when app is 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 = KingfisherWrapper<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: Image Cache State
|
|
|
|
/// Returns the cache type for a given `key` and `identifier` combination.
|
|
/// This method is used for checking whether an image is cached in current cache.
|
|
/// It also provides information on which kind of cache can it be found in the return value.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: The key used for caching the image.
|
|
/// - identifier: Processor identifier which used for this image. Default is the `identifier` of
|
|
/// `DefaultImageProcessor.default`.
|
|
/// - Returns: A `CacheType` instance which indicates the cache status.
|
|
/// `.none` means the image is not in cache or it is already expired.
|
|
open func imageCachedType(
|
|
forKey key: String,
|
|
processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> CacheType
|
|
{
|
|
let computedKey = key.computedKey(with: identifier)
|
|
if memoryStorage.isCached(forKey: computedKey) { return .memory }
|
|
if diskStorage.isCached(forKey: computedKey) { return .disk }
|
|
return .none
|
|
}
|
|
|
|
/// Returns whether the file exists in cache for a given `key` and `identifier` combination.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: The key used for caching the image.
|
|
/// - identifier: Processor identifier which used for this image. Default is the `identifier` of
|
|
/// `DefaultImageProcessor.default`.
|
|
/// - Returns: A `Bool` which indicates whether a cache could match the given `key` and `identifier` combination.
|
|
///
|
|
/// - Note:
|
|
/// The return value does not contain information about from which kind of storage the cache matches.
|
|
/// To get the information about cache type according `CacheType`,
|
|
/// use `imageCachedType(forKey:processorIdentifier:)` instead.
|
|
public func isCached(
|
|
forKey key: String,
|
|
processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> Bool
|
|
{
|
|
return imageCachedType(forKey: key, processorIdentifier: identifier).cached
|
|
}
|
|
|
|
/// Gets the hash used as cache file name for the key.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: The key used for caching the image.
|
|
/// - identifier: Processor identifier which used for this image. Default is the `identifier` of
|
|
/// `DefaultImageProcessor.default`.
|
|
/// - Returns: The hash which is used as the cache file name.
|
|
///
|
|
/// - Note:
|
|
/// By default, for a given combination of `key` and `identifier`, `ImageCache` will use the value
|
|
/// returned by this method as the cache file name. You can use this value to check and match cache file
|
|
/// if you need.
|
|
open func hash(
|
|
forKey key: String,
|
|
processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String
|
|
{
|
|
let computedKey = key.computedKey(with: identifier)
|
|
return diskStorage.cacheFileName(forKey: computedKey)
|
|
}
|
|
|
|
/// Calculates the size taken by the disk storage.
|
|
/// It is the total file size of all cached files in the `diskStorage` on disk in bytes.
|
|
///
|
|
/// - Parameter handler: Called with the size calculating finishes. This closure is invoked from the main queue.
|
|
open func calculateDiskStorageSize(completion handler: @escaping ((Result<UInt, KingfisherError>) -> Void)) {
|
|
ioQueue.async {
|
|
do {
|
|
let size = try self.diskStorage.totalSize()
|
|
DispatchQueue.main.async { handler(.success(size)) }
|
|
} catch {
|
|
if let error = error as? KingfisherError {
|
|
DispatchQueue.main.async { handler(.failure(error)) }
|
|
} else {
|
|
assertionFailure("The internal thrown error should be a `KingfisherError`.")
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Gets the cache path for the key.
|
|
/// It is useful for projects with web view or anyone that needs access to the local file path.
|
|
///
|
|
/// i.e. Replacing the `<img src='path_for_key'>` tag in your HTML.
|
|
///
|
|
/// - Parameters:
|
|
/// - key: The key used for caching the image.
|
|
/// - identifier: Processor identifier which used for this image. Default is the `identifier` of
|
|
/// `DefaultImageProcessor.default`.
|
|
/// - Returns: The disk path of cached image under the given `key` and `identifier`.
|
|
///
|
|
/// - Note:
|
|
/// This method does not guarantee there is an image already cached in the returned path. It just gives your
|
|
/// the path that the image should be, if it exists in disk storage.
|
|
///
|
|
/// You could use `isCached(forKey:)` method to check whether the image is cached under that key in disk.
|
|
open func cachePath(
|
|
forKey key: String,
|
|
processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String
|
|
{
|
|
let computedKey = key.computedKey(with: identifier)
|
|
return diskStorage.cacheFileURL(forKey: computedKey).path
|
|
}
|
|
}
|
|
|
|
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 KingfisherWrapper 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)")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ImageCache {
|
|
|
|
/// Creates an `ImageCache` with a given `name`, cache directory `path`
|
|
/// and a closure to modify the cache directory.
|
|
///
|
|
/// - Parameters:
|
|
/// - name: The name of cache object. It is used to setup disk cache directories and IO queue.
|
|
/// You should not use the same `name` for different caches, otherwise, the disk storage would
|
|
/// be conflicting to each other.
|
|
/// - path: Location of cache URL on disk. It will be internally pass to the initializer of `DiskStorage` as the
|
|
/// disk cache directory.
|
|
/// - 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.
|
|
/// - Throws: An error that happens during image cache creating, such as unable to create a directory at the given
|
|
/// path.
|
|
@available(*, deprecated, message: "Use `init(name:cacheDirectoryURL:diskCachePathClosure:)` instead",
|
|
renamed: "init(name:cacheDirectoryURL:diskCachePathClosure:)")
|
|
public convenience init(
|
|
name: String,
|
|
path: String?,
|
|
diskCachePathClosure: DiskCachePathClosure? = nil) throws
|
|
{
|
|
let directoryURL = path.flatMap { URL(string: $0) }
|
|
try self.init(name: name, cacheDirectoryURL: directoryURL, diskCachePathClosure: diskCachePathClosure)
|
|
}
|
|
}
|