Browse Source

Clean keys when memory cache evicts object

onevcat 7 years ago
parent
commit
e8e3a37651

+ 7 - 5
Demo/Demo/Kingfisher-Demo/ViewControllers/HighResolutionCollectionViewController.swift

@@ -56,21 +56,23 @@ class HighResolutionCollectionViewController: UICollectionViewController {
         willDisplay cell: UICollectionViewCell,
         forItemAt indexPath: IndexPath)
     {
+        let imageView = (cell as! ImageCollectionViewCell).cellImageView!
         let url = ImageLoader.highResolutionImageURLs[indexPath.row % ImageLoader.highResolutionImageURLs.count]
         // Use different cache key to prevent reuse the same image. It is just for
         // this demo. Normally you can just use the URL to set image.
         let resource = ImageResource(downloadURL: url, cacheKey: "\(url.absoluteString)-\(indexPath.row)")
         
         // This should crash most devices due to memory pressure.
-        // (cell as! ImageCollectionViewCell).cellImageView.kf.setImage(with: resource)
-        
-        // This would survive on even low spec devices!
-        (cell as! ImageCollectionViewCell).cellImageView.kf.setImage(
+        // imageView.kf.setImage(with: resource)
+
+        // This would survive on even the lowest spec devices!
+        imageView.kf.setImage(
             with: resource,
             options: [
                 .processor(DownsamplingImageProcessor(size: CGSize(width: 250, height: 250))),
                 .cacheOriginalImage
-        ])
+            ]
+        )
     }
     
     override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

+ 21 - 2
Sources/Cache/MemoryStorage.swift

@@ -48,6 +48,8 @@ public enum MemoryStorage {
         var cleanTimer: Timer? = nil
         let lock = NSLock()
 
+        let cacheDelegate = CacheDelegate<StorageObject<T>>()
+
         /// The config used in this storage. It is a value you can set and
         /// use to config the storage in air.
         public var config: Config {
@@ -65,6 +67,10 @@ public enum MemoryStorage {
             self.config = config
             storage.totalCostLimit = config.totalCostLimit
             storage.countLimit = config.countLimit
+            storage.delegate = cacheDelegate
+            cacheDelegate.onObjectRemoved.delegate(on: self) { (self, obj) in
+                self.keys.remove(obj.key)
+            }
 
             cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in
                 guard let self = self else { return }
@@ -111,7 +117,7 @@ public enum MemoryStorage {
             // The expiration indicates that already expired, no need to store.
             guard !expiration.isExpired else { return }
             
-            let object = StorageObject(value, expiration: expiration)
+            let object = StorageObject(value, key: key, expiration: expiration)
             storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
             keys.insert(key)
         }
@@ -153,6 +159,17 @@ public enum MemoryStorage {
             storage.removeAllObjects()
             keys.removeAll()
         }
+
+        class CacheDelegate<T>: NSObject, NSCacheDelegate {
+            
+            let onObjectRemoved = Delegate<T, Void>()
+            
+            func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any) {
+                if let obj = obj as? T {
+                    onObjectRemoved.call(obj)
+                }
+            }
+        }
     }
 }
 
@@ -193,11 +210,13 @@ extension MemoryStorage {
     class StorageObject<T> {
         let value: T
         let expiration: StorageExpiration
+        let key: String
         
         private(set) var estimatedExpiration: Date
         
-        init(_ value: T, expiration: StorageExpiration) {
+        init(_ value: T, key: String, expiration: StorageExpiration) {
             self.value = value
+            self.key = key
             self.expiration = expiration
             
             self.estimatedExpiration = expiration.estimatedExpirationSinceNow

+ 10 - 8
Sources/General/ImageSource/ImageDataProvider.swift

@@ -58,7 +58,10 @@ public struct LocalFileImageDataProvider: ImageDataProvider {
 
     /// The file URL from which the image be loaded.
     public let fileURL: URL
-    
+
+    /// The key used in cache.
+    public var cacheKey: String
+
     /// Creates an image data provider by supplying the target local file URL.
     ///
     /// - Parameters:
@@ -69,9 +72,7 @@ public struct LocalFileImageDataProvider: ImageDataProvider {
         self.fileURL = fileURL
         self.cacheKey = cacheKey ?? fileURL.absoluteString
     }
-    
-    /// The key used in cache.
-    public var cacheKey: String
+
     public func data(handler: (Result<Data, Error>) -> Void) {
         handler( Result { try Data(contentsOf: fileURL) } )
     }
@@ -80,7 +81,10 @@ public struct LocalFileImageDataProvider: ImageDataProvider {
 /// Represents an image data provider for loading image from a given Base64 encoded string.
 public struct Base64ImageDataProvider: ImageDataProvider {
     
-    let base64String: String
+    public let base64String: String
+
+    /// The key used in cache.
+    public var cacheKey: String
     
     /// Creates an image data provider by supplying the Base64 encoded string.
     ///
@@ -92,8 +96,6 @@ public struct Base64ImageDataProvider: ImageDataProvider {
         self.cacheKey = cacheKey
     }
 
-    /// The key used in cache.
-    public var cacheKey: String
     public func data(handler: (Result<Data, Error>) -> Void) {
         let data = Data(base64Encoded: base64String)!
         handler(.success(data))
@@ -107,7 +109,7 @@ public struct RawImageDataProvider: ImageDataProvider {
     public let data: Data
     
     /// The key used in cache.
-    public let cacheKey: String
+    public var cacheKey: String
     
     /// Creates an image data provider by the given raw `data` value and a `cacheKey` be used in Kingfisher cache.
     ///

+ 2 - 2
Sources/General/ImageSource/Source.swift

@@ -55,14 +55,14 @@ public enum Source {
     /// from local storage or in any other encoding format (like Base64).
     case provider(ImageDataProvider)
     
-    var cacheKey: String {
+    public var cacheKey: String {
         switch self {
         case .network(let resource): return resource.cacheKey
         case .provider(let provider): return provider.cacheKey
         }
     }
     
-    var url: URL? {
+    public var url: URL? {
         switch self {
         case .network(let resource): return resource.downloadURL
         // `ImageDataProvider` does not provide a URL. All it cares is how to get the data back.

+ 8 - 1
Sources/General/KingfisherError.swift

@@ -210,7 +210,14 @@ public enum KingfisherError: Error {
         }
         return false
     }
-    
+
+    public var isInvalidResponseStatusCode: Bool {
+        if case .responseError(reason: .invalidHTTPStatusCode) = self {
+            return true
+        }
+        return false
+    }
+
     /// Helper property to check whether this error is a `ImageSettingErrorReason.notCurrentSourceTask` or not.
     /// When a new image setting task starts while the old one is still running, the new task identifier will be
     /// set and the old one is overwritten. A `.notCurrentSourceTask` error will be raised when the old task finishes

+ 0 - 1
Sources/Networking/ImageDownloader.swift

@@ -303,7 +303,6 @@ open class ImageDownloader {
     }
 }
 
-// MARK: - Download method
 extension ImageDownloader {
 
     /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers

+ 4 - 2
Tests/KingfisherTests/MemoryStorageTests.swift

@@ -191,10 +191,12 @@ class MemoryStorageTests: XCTestCase {
 
         try! storage.store(value: 1, forKey: "1", expiration: .seconds(0.1))
         XCTAssertTrue(storage.isCached(forKey: "1"))
-
+        XCTAssertEqual(self.storage.keys.count, 1)
+        
         delay(0.2) {
             XCTAssertFalse(self.storage.isCached(forKey: "1"))
             XCTAssertNil(self.storage.storage.object(forKey: "1"))
+            XCTAssertEqual(self.storage.keys.count, 0)
             exp.fulfill()
         }
 
@@ -203,7 +205,7 @@ class MemoryStorageTests: XCTestCase {
 
     func testStorageObject() {
         let now = Date()
-        let obj = MemoryStorage.StorageObject(1, expiration: .seconds(1))
+        let obj = MemoryStorage.StorageObject(1, key: "1", expiration: .seconds(1))
         XCTAssertEqual(obj.value, 1)
 
         XCTAssertEqual(