Răsfoiți Sursa

Add options to specify expiration

onevcat 7 ani în urmă
părinte
comite
a837003559

+ 5 - 1
Sources/Cache/DiskStorage.swift

@@ -96,6 +96,10 @@ public enum DiskStorage {
             forKey key: String,
             expiration: StorageExpiration? = nil) throws
         {
+            let expiration = expiration ?? config.expiration
+            // The expiration indicates that already expired, no need to store.
+            guard !expiration.isExpired else { return }
+            
             let data: Data
             do {
                 data = try value.toData()
@@ -110,7 +114,7 @@ public enum DiskStorage {
                 // The last access date.
                 .creationDate: now.fileAttributeDate,
                 // The estimated expiration date.
-                .modificationDate: (expiration ?? config.expiration).estimatedExpirationSinceNow.fileAttributeDate
+                .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
             ]
             config.fileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes)
         }

+ 65 - 33
Sources/Cache/ImageCache.swift

@@ -246,6 +246,51 @@ open class ImageCache {
     deinit {
         NotificationCenter.default.removeObserver(self)
     }
+    
+    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) }
+            }
+        }
+    }
 
     // MARK: - Store & Remove
     /// Stores an image to the cache.
@@ -278,67 +323,53 @@ open class ImageCache {
                       callbackQueue: CallbackQueue = .untouch,
                       completionHandler: ((CacheStoreResult) -> Void)? = nil)
     {
-        let computedKey = key.computedKey(with: identifier)
-        // Memory storage should not throw.
-        memoryStorage.storeNoThrow(value: image, forKey: computedKey)
-
-        guard toDisk else {
-            if let completionHandler = completionHandler {
-                let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
-                callbackQueue.execute { completionHandler(result) }
+        struct TempProcessor: ImageProcessor {
+            let identifier: String
+            func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? {
+                return nil
             }
-            return
         }
         
-        ioQueue.async {
-            if let data = serializer.data(with: image, original: original) {
-                self.syncStoreData(
-                    data,
-                    forKey: key,
-                    processorIdentifier: identifier,
-                    callbackQueue: callbackQueue,
-                    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) }
-            }
-        }
+        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.syncStoreData(
+            self.syncStoreToDisk(
                 data,
                 forKey: key,
                 processorIdentifier: identifier,
                 callbackQueue: callbackQueue,
+                expiration: expiration,
                 completionHandler: completionHandler)
         }
     }
     
-    private func syncStoreData(
+    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)
+            try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration)
             result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
         } catch {
             let diskError: KingfisherError
@@ -428,11 +459,12 @@ open class ImageCache {
                     // 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(
                         image,
                         forKey: key,
-                        processorIdentifier: options.processor.identifier,
-                        cacheSerializer: options.cacheSerializer,
+                        options: cacheOptions,
                         toDisk: false)
                     {
                         _ in

+ 5 - 1
Sources/Cache/MemoryStorage.swift

@@ -107,7 +107,11 @@ public enum MemoryStorage {
         {
             lock.lock()
             defer { lock.unlock() }
-            let object = StorageObject(value, expiration: expiration ?? config.expiration)
+            let expiration = expiration ?? config.expiration
+            // The expiration indicates that already expired, no need to store.
+            guard !expiration.isExpired else { return }
+            
+            let object = StorageObject(value, expiration: expiration)
             storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
             keys.insert(key)
         }

+ 8 - 0
Sources/Cache/Storage.swift

@@ -41,6 +41,8 @@ public enum StorageExpiration {
     case days(Int)
     /// The item expires after a given date.
     case date(Date)
+    /// Indicates the item is already expired. Use this to skip cache.
+    case expired
 
     func estimatedExpirationSince(_ date: Date) -> Date {
         switch self {
@@ -48,12 +50,17 @@ public enum StorageExpiration {
         case .seconds(let seconds): return date.addingTimeInterval(seconds)
         case .days(let days): return date.addingTimeInterval(TimeInterval(60 * 60 * 24 * days))
         case .date(let ref): return ref
+        case .expired: return .distantPast
         }
     }
     
     var estimatedExpirationSinceNow: Date {
         return estimatedExpirationSince(Date())
     }
+    
+    var isExpired: Bool {
+        return timeInterval <= 0
+    }
 
     var timeInterval: TimeInterval {
         switch self {
@@ -61,6 +68,7 @@ public enum StorageExpiration {
         case .seconds(let seconds): return seconds
         case .days(let days): return TimeInterval(60 * 60 * 24 * days)
         case .date(let ref): return ref.timeIntervalSinceNow
+        case .expired: return -(.infinity)
         }
     }
 }

+ 7 - 7
Sources/General/KingfisherManager.swift

@@ -223,10 +223,8 @@ public class KingfisherManager {
                     value.image,
                     original: value.originalData,
                     forKey: source.cacheKey,
-                    processorIdentifier: options.processor.identifier,
-                    cacheSerializer: options.cacheSerializer,
-                    toDisk: !options.cacheMemoryOnly,
-                    callbackQueue: options.callbackQueue)
+                    options: options,
+                    toDisk: !options.cacheMemoryOnly)
                 {
                     _ in
                     if options.waitForCache {
@@ -244,7 +242,8 @@ public class KingfisherManager {
                         originalCache.storeToDisk(
                             value.originalData,
                             forKey: source.cacheKey,
-                            processorIdentifier: DefaultImageProcessor.default.identifier)
+                            processorIdentifier: DefaultImageProcessor.default.identifier,
+                            expiration: options.diskCacheExpiration)
                     }
                 }
 
@@ -344,11 +343,12 @@ public class KingfisherManager {
                             return
                         }
 
+                        var cacheOptions = options
+                        cacheOptions.callbackQueue = .untouch
                         targetCache.store(
                             processedImage,
                             forKey: key,
-                            processorIdentifier: processor.identifier,
-                            cacheSerializer: options.cacheSerializer,
+                            options: cacheOptions,
                             toDisk: !options.cacheMemoryOnly)
                         {
                             _ in

+ 14 - 0
Sources/General/KingfisherOptionsInfo.swift

@@ -186,6 +186,16 @@ public enum KingfisherOptionsInfoItem {
     /// Set this options will stop that flickering by keeping all loading in the same queue (typically the UI queue
     /// if you are using Kingfisher's extension methods to set an image), with a tradeoff of loading performance.
     case loadDiskFileSynchronously
+    
+    /// The expiration setting for memory cache. By default, the underlying `MemoryStorage.Backend` uses the
+    /// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this associated
+    /// value to overwrite the config setting for this caching item.
+    case memoryCacheExpiration(StorageExpiration)
+    
+    /// The expiration setting for memory cache. By default, the underlying `DiskStorage.Backend` uses the
+    /// expiration in its config for all items. If set, the `DiskStorage.Backend` will use this associated
+    /// value to overwrite the config setting for this caching item.
+    case diskCacheExpiration(StorageExpiration)
 }
 
 // Improve performance by parsing the input `KingfisherOptionsInfo` (self) first.
@@ -217,6 +227,8 @@ public struct KingfisherParsedOptionsInfo {
     public var onFailureImage: Optional<Image?> = .none
     public var alsoPrefetchToMemory = false
     public var loadDiskFileSynchronously = false
+    public var memoryCacheExpiration: StorageExpiration? = nil
+    public var diskCacheExpiration: StorageExpiration? = nil
 
     public init(_ info: KingfisherOptionsInfo?) {
         guard let info = info else { return }
@@ -248,6 +260,8 @@ public struct KingfisherParsedOptionsInfo {
             case .alsoPrefetchToMemory: alsoPrefetchToMemory = true
             case .loadDiskFileSynchronously: loadDiskFileSynchronously = true
             case .callbackDispatchQueue(let value): callbackQueue = value.map { .dispatch($0) } ?? .mainCurrentOrAsync
+            case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration
+            case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration
             }
         }
 

+ 40 - 1
Tests/KingfisherTests/ImageCacheTests.swift

@@ -258,7 +258,7 @@ class ImageCacheTests: XCTestCase {
         let exp = expectation(description: #function)
         let key = testKeys[0]
 
-        cache.diskStorage.config.expiration = .seconds(0)
+        cache.diskStorage.config.expiration = .seconds(0.01)
 
         cache.store(testImage, original: testImageData, forKey: key, toDisk: true) { _ in
             self.observer = NotificationCenter.default.addObserver(
@@ -425,6 +425,45 @@ class ImageCacheTests: XCTestCase {
         waitForExpectations(timeout: 1, handler: nil)
     }
 #endif
+    
+    func testStoreToMemoryWithExpiration() {
+        let exp = expectation(description: #function)
+        let key = testKeys[0]
+        cache.store(
+            testImage,
+            original: testImageData,
+            forKey: key,
+            options: KingfisherParsedOptionsInfo([.memoryCacheExpiration(.seconds(0.2))]),
+            toDisk: true)
+        {
+            _ in
+            XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)
+            delay(1) {
+                XCTAssertEqual(self.cache.imageCachedType(forKey: key), .disk)
+                exp.fulfill()
+            }
+        }
+        waitForExpectations(timeout: 1.5, handler: nil)
+    }
+    
+    func testStoreToDiskWithExpiration() {
+        let exp = expectation(description: #function)
+        let key = testKeys[0]
+        cache.store(
+            testImage,
+            original: testImageData,
+            forKey: key,
+            options: KingfisherParsedOptionsInfo([.diskCacheExpiration(.expired)]),
+            toDisk: true)
+        {
+            _ in
+            XCTAssertEqual(self.cache.imageCachedType(forKey: key), .memory)
+            self.cache.clearMemoryCache()
+            XCTAssertEqual(self.cache.imageCachedType(forKey: key), .none)
+            exp.fulfill()
+        }
+        waitForExpectations(timeout: 1.5, handler: nil)
+    }
 
     // MARK: - Helper
     func storeMultipleImages(_ completionHandler: @escaping () -> Void) {

+ 10 - 1
Tests/KingfisherTests/StorageExpirationTests.swift

@@ -33,6 +33,7 @@ class StorageExpirationTests: XCTestCase {
         let e = StorageExpiration.never
         XCTAssertEqual(e.estimatedExpirationSinceNow, .distantFuture)
         XCTAssertEqual(e.timeInterval, .infinity)
+        XCTAssertFalse(e.isExpired)
     }
 
     func testExpirationSeconds() {
@@ -42,6 +43,7 @@ class StorageExpirationTests: XCTestCase {
             Date().timeIntervalSince1970 + 100,
             accuracy: 0.1)
         XCTAssertEqual(e.timeInterval, 100)
+        XCTAssertFalse(e.isExpired)
     }
     
     func testExpirationDays() {
@@ -52,6 +54,7 @@ class StorageExpirationTests: XCTestCase {
             Date().timeIntervalSince1970 + oneDayInSecond,
             accuracy: 0.1)
         XCTAssertEqual(e.timeInterval, oneDayInSecond, accuracy: 0.1)
+        XCTAssertFalse(e.isExpired)
     }
     
     func testExpirationDate() {
@@ -63,6 +66,12 @@ class StorageExpirationTests: XCTestCase {
             Date().timeIntervalSince1970 + oneDayInSecond,
             accuracy: 0.1)
         XCTAssertEqual(e.timeInterval, oneDayInSecond, accuracy: 0.1)
+        XCTAssertFalse(e.isExpired)
+    }
+    
+    func testAlreadyExpired() {
+        let e = StorageExpiration.expired
+        XCTAssertTrue(e.isExpired)
+        XCTAssertEqual(e.estimatedExpirationSinceNow, .distantPast)
     }
-
 }