| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- //
- // MemoryStorage.swift
- // Kingfisher
- //
- // Created by Wei Wang on 2018/10/15.
- //
- // 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.
- import Foundation
- /// Represents the concepts related to storage that stores a specific type of value in memory.
- ///
- /// This serves as a namespace for memory storage types. A ``MemoryStorage/Backend`` with a particular
- /// ``MemoryStorage/Config`` is used to define the storage.
- ///
- /// Refer to these composite types for further details.
- public enum MemoryStorage {
- /// Represents a storage that stores a specific type of value in memory.
- ///
- /// It provides fast access but has a limited storage size. The stored value type needs to conform to the
- /// ``CacheCostCalculable`` protocol, and its ``CacheCostCalculable/cacheCost`` will be used to determine the cost
- /// of the cache item's size in the memory.
- ///
- /// You can configure a ``MemoryStorage/Backend`` in its ``MemoryStorage/Backend/init(config:)`` method by passing
- /// a ``MemoryStorage/Config`` value or by modifying the ``MemoryStorage/Backend/config`` property after it's
- /// created.
- ///
- /// The ``MemoryStorage`` backend has an upper limit on the total cost size in memory and item count. All items in
- /// the storage have an expiration date. When retrieved, if the target item is already expired, it will be
- /// recognized as if it does not exist in the storage.
- ///
- /// The `MemoryStorage` also includes a scheduled self-cleaning task to evict expired items from memory.
- ///
- /// > This class is thready safe.
- public final class Backend<T: CacheCostCalculable>: @unchecked Sendable where T: Sendable {
-
- let storage = NSCache<NSString, StorageObject<T>>()
- // Keys track the objects once inside the storage.
- //
- // For object removing triggered by user, the corresponding key would be also removed. However, for the object
- // removing triggered by cache rule/policy of system, the key will be remained there until next `removeExpired`
- // happens.
- //
- // Breaking the strict tracking could save additional locking behaviors and improve the cache performance.
- // See https://github.com/onevcat/Kingfisher/issues/1233
- var keys = Set<String>()
- private var cleanTimer: Timer? = nil
- private let lock = NSLock()
- /// The configuration used in this storage.
- ///
- /// It is a value you can set and use to configure the storage as needed.
- public var config: Config {
- didSet {
- storage.totalCostLimit = config.totalCostLimit
- storage.countLimit = config.countLimit
- cleanTimer?.invalidate()
- cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in
- guard let self = self else { return }
- self.removeExpired()
- }
- }
- }
- /// Creates a ``MemoryStorage/Backend`` with a given ``MemoryStorage/Config`` value.
- ///
- /// - Parameter config: The configuration used to create the storage. It determines the maximum size limitation,
- /// default expiration settings, and more.
- public init(config: Config) {
- self.config = config
- storage.totalCostLimit = config.totalCostLimit
- storage.countLimit = config.countLimit
- cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in
- guard let self = self else { return }
- self.removeExpired()
- }
- }
- /// Removes the expired values from the storage.
- public func removeExpired() {
- lock.lock()
- defer { lock.unlock() }
- for key in keys {
- let nsKey = key as NSString
- guard let object = storage.object(forKey: nsKey) else {
- // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule.
- // We didn't remove the key yet until now, since we do not want to introduce additional lock.
- // See https://github.com/onevcat/Kingfisher/issues/1233
- keys.remove(key)
- continue
- }
- if object.isExpired {
- storage.removeObject(forKey: nsKey)
- keys.remove(key)
- }
- }
- }
-
- /// Stores a value in the storage under the specified key and expiration policy.
- ///
- /// - Parameters:
- /// - value: The value to be stored.
- /// - key: The key to which the `value` will be stored.
- /// - expiration: The expiration policy used by this storage action.
- public func store(
- value: T,
- forKey key: String,
- expiration: StorageExpiration? = nil)
- {
- storeNoThrow(value: value, forKey: key, expiration: expiration)
- }
- // The no throw version for storing value in cache. Kingfisher knows the detail so it
- // could use this version to make syntax simpler internally.
- func storeNoThrow(
- value: T,
- forKey key: String,
- expiration: StorageExpiration? = nil)
- {
- lock.lock()
- defer { lock.unlock() }
- let expiration = expiration ?? config.expiration
- // The expiration indicates that already expired, no need to store.
- guard !expiration.isExpired else { return }
-
- let object: StorageObject<T>
- if config.keepWhenEnteringBackground {
- object = BackgroundKeepingStorageObject(value, expiration: expiration)
- } else {
- object = StorageObject(value, expiration: expiration)
- }
- storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
- keys.insert(key)
- }
-
- /// Gets a value from the storage.
- ///
- /// - Parameters:
- /// - key: The cache key of the value.
- /// - extendingExpiration: The expiration policy used by this retrieval action.
- /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`.
- public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
- guard let object = storage.object(forKey: key as NSString) else {
- return nil
- }
- if object.isExpired {
- return nil
- }
- object.extendExpiration(extendingExpiration)
- return object.value
- }
- /// Determines whether there is valid cached data under a given key.
- ///
- /// - Parameter key: The cache key of the value.
- /// - Returns: `true` if there is valid data under the key, otherwise `false`.
- public func isCached(forKey key: String) -> Bool {
- guard let _ = value(forKey: key, extendingExpiration: .none) else {
- return false
- }
- return true
- }
- /// Removes a value from a specified key.
- ///
- /// - Parameter key: The cache key of the value.
- public func remove(forKey key: String) {
- lock.lock()
- defer { lock.unlock() }
- storage.removeObject(forKey: key as NSString)
- keys.remove(key)
- }
- /// Removes all values in this storage.
- public func removeAll() {
- lock.lock()
- defer { lock.unlock() }
- storage.removeAllObjects()
- keys.removeAll()
- }
- }
- }
- extension MemoryStorage {
- /// Represents the configuration used in a ``MemoryStorage/Backend``.
- public struct Config {
- /// The total cost limit of the storage.
- ///
- /// This counts up the value of ``CacheCostCalculable/cacheCost``. If adding this object to the cache causes
- /// the cache’s total cost to rise above totalCostLimit, the cache may automatically evict objects until its
- /// total cost falls below this value.
- public var totalCostLimit: Int
- /// The item count limit of the memory storage.
- ///
- /// The default value is `Int.max`, which means no hard limitation of the item count.
- public var countLimit: Int = .max
- /// The ``StorageExpiration`` used in this memory storage.
- ///
- /// The default is `.seconds(300)`, which means that the memory cache will expire in 5 minutes if not accessed.
- public var expiration: StorageExpiration = .seconds(300)
- /// The time interval between the storage performing cleaning work for sweeping expired items.
- public var cleanInterval: TimeInterval
-
- /// Determine whether newly added items to memory cache should be purged when the app goes to the background.
- ///
- /// By default, cached items in memory will be purged as soon as the app goes to the background to ensure a
- /// minimal memory footprint. Enabling this prevents this behavior and keeps the items alive in the cache even
- /// when your app is not in the foreground.
- ///
- /// The default value is `false`. After setting it to `true`, only newly added cache objects are affected.
- /// Existing objects that were already in the cache while this value was `false` will still be purged when the
- /// app enters the background.
- public var keepWhenEnteringBackground: Bool = false
- /// Creates a configuration from a given ``MemoryStorage/Config/totalCostLimit`` value and a
- /// ``MemoryStorage/Config/cleanInterval``.
- ///
- /// - Parameters:
- /// - totalCostLimit: The total cost limit of the storage in bytes.
- /// - cleanInterval: The time interval between the storage performing cleaning work for sweeping expired items.
- /// The default is 120, which means auto eviction happens once every two minutes.
- ///
- /// > Other properties of the ``MemoryStorage/Config`` will use their default values when created.
- public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) {
- self.totalCostLimit = totalCostLimit
- self.cleanInterval = cleanInterval
- }
- }
- }
- extension MemoryStorage {
-
- class BackgroundKeepingStorageObject<T>: StorageObject<T>, NSDiscardableContent {
- var accessing = true
- func beginContentAccess() -> Bool {
- if value != nil {
- accessing = true
- } else {
- accessing = false
- }
- return accessing
- }
-
- func endContentAccess() {
- accessing = false
- }
-
- func discardContentIfPossible() {
- value = nil
- }
-
- func isContentDiscarded() -> Bool {
- return value == nil
- }
- }
-
- class StorageObject<T> {
- var value: T?
- let expiration: StorageExpiration
-
- private(set) var estimatedExpiration: Date
-
- init(_ value: T, expiration: StorageExpiration) {
- self.value = value
- self.expiration = expiration
-
- self.estimatedExpiration = expiration.estimatedExpirationSinceNow
- }
- func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) {
- switch extendingExpiration {
- case .none:
- return
- case .cacheTime:
- self.estimatedExpiration = expiration.estimatedExpirationSinceNow
- case .expirationTime(let expirationTime):
- self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow
- }
- }
-
- var isExpired: Bool {
- return estimatedExpiration.isPast
- }
- }
- }
|