MemoryStorage.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. //
  2. // MemoryStorage.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 2018/10/15.
  6. //
  7. // Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import Foundation
  27. /// Represents the concepts related to storage that stores a specific type of value in memory.
  28. ///
  29. /// This serves as a namespace for memory storage types. A ``MemoryStorage/Backend`` with a particular
  30. /// ``MemoryStorage/Config`` is used to define the storage.
  31. ///
  32. /// Refer to these composite types for further details.
  33. public enum MemoryStorage {
  34. /// Represents a storage that stores a specific type of value in memory.
  35. ///
  36. /// It provides fast access but has a limited storage size. The stored value type needs to conform to the
  37. /// ``CacheCostCalculable`` protocol, and its ``CacheCostCalculable/cacheCost`` will be used to determine the cost
  38. /// of the cache item's size in the memory.
  39. ///
  40. /// You can configure a ``MemoryStorage/Backend`` in its ``MemoryStorage/Backend/init(config:)`` method by passing
  41. /// a ``MemoryStorage/Config`` value or by modifying the ``MemoryStorage/Backend/config`` property after it's
  42. /// created.
  43. ///
  44. /// The ``MemoryStorage`` backend has an upper limit on the total cost size in memory and item count. All items in
  45. /// the storage have an expiration date. When retrieved, if the target item is already expired, it will be
  46. /// recognized as if it does not exist in the storage.
  47. ///
  48. /// The `MemoryStorage` also includes a scheduled self-cleaning task to evict expired items from memory.
  49. ///
  50. /// > This class is thready safe.
  51. public final class Backend<T: CacheCostCalculable>: @unchecked Sendable where T: Sendable {
  52. let storage = NSCache<NSString, StorageObject<T>>()
  53. // Keys track the objects once inside the storage.
  54. //
  55. // For object removing triggered by user, the corresponding key would be also removed. However, for the object
  56. // removing triggered by cache rule/policy of system, the key will be remained there until next `removeExpired`
  57. // happens.
  58. //
  59. // Breaking the strict tracking could save additional locking behaviors and improve the cache performance.
  60. // See https://github.com/onevcat/Kingfisher/issues/1233
  61. var keys = Set<String>()
  62. private var cleanTimer: Timer? = nil
  63. private let lock = NSLock()
  64. /// The configuration used in this storage.
  65. ///
  66. /// It is a value you can set and use to configure the storage as needed.
  67. public var config: Config {
  68. didSet {
  69. storage.totalCostLimit = config.totalCostLimit
  70. storage.countLimit = config.countLimit
  71. cleanTimer?.invalidate()
  72. cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in
  73. guard let self = self else { return }
  74. self.removeExpired()
  75. }
  76. }
  77. }
  78. /// Creates a ``MemoryStorage/Backend`` with a given ``MemoryStorage/Config`` value.
  79. ///
  80. /// - Parameter config: The configuration used to create the storage. It determines the maximum size limitation,
  81. /// default expiration settings, and more.
  82. public init(config: Config) {
  83. self.config = config
  84. storage.totalCostLimit = config.totalCostLimit
  85. storage.countLimit = config.countLimit
  86. cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in
  87. guard let self = self else { return }
  88. self.removeExpired()
  89. }
  90. }
  91. /// Removes the expired values from the storage.
  92. public func removeExpired() {
  93. lock.lock()
  94. defer { lock.unlock() }
  95. for key in keys {
  96. let nsKey = key as NSString
  97. guard let object = storage.object(forKey: nsKey) else {
  98. // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule.
  99. // We didn't remove the key yet until now, since we do not want to introduce additional lock.
  100. // See https://github.com/onevcat/Kingfisher/issues/1233
  101. keys.remove(key)
  102. continue
  103. }
  104. if object.isExpired {
  105. storage.removeObject(forKey: nsKey)
  106. keys.remove(key)
  107. }
  108. }
  109. }
  110. /// Stores a value in the storage under the specified key and expiration policy.
  111. ///
  112. /// - Parameters:
  113. /// - value: The value to be stored.
  114. /// - key: The key to which the `value` will be stored.
  115. /// - expiration: The expiration policy used by this storage action.
  116. public func store(
  117. value: T,
  118. forKey key: String,
  119. expiration: StorageExpiration? = nil)
  120. {
  121. storeNoThrow(value: value, forKey: key, expiration: expiration)
  122. }
  123. // The no throw version for storing value in cache. Kingfisher knows the detail so it
  124. // could use this version to make syntax simpler internally.
  125. func storeNoThrow(
  126. value: T,
  127. forKey key: String,
  128. expiration: StorageExpiration? = nil)
  129. {
  130. lock.lock()
  131. defer { lock.unlock() }
  132. let expiration = expiration ?? config.expiration
  133. // The expiration indicates that already expired, no need to store.
  134. guard !expiration.isExpired else { return }
  135. let object: StorageObject<T>
  136. if config.keepWhenEnteringBackground {
  137. object = BackgroundKeepingStorageObject(value, expiration: expiration)
  138. } else {
  139. object = StorageObject(value, expiration: expiration)
  140. }
  141. storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
  142. keys.insert(key)
  143. }
  144. /// Gets a value from the storage.
  145. ///
  146. /// - Parameters:
  147. /// - key: The cache key of the value.
  148. /// - extendingExpiration: The expiration policy used by this retrieval action.
  149. /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`.
  150. public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
  151. guard let object = storage.object(forKey: key as NSString) else {
  152. return nil
  153. }
  154. if object.isExpired {
  155. return nil
  156. }
  157. object.extendExpiration(extendingExpiration)
  158. return object.value
  159. }
  160. /// Determines whether there is valid cached data under a given key.
  161. ///
  162. /// - Parameter key: The cache key of the value.
  163. /// - Returns: `true` if there is valid data under the key, otherwise `false`.
  164. public func isCached(forKey key: String) -> Bool {
  165. guard let _ = value(forKey: key, extendingExpiration: .none) else {
  166. return false
  167. }
  168. return true
  169. }
  170. /// Removes a value from a specified key.
  171. ///
  172. /// - Parameter key: The cache key of the value.
  173. public func remove(forKey key: String) {
  174. lock.lock()
  175. defer { lock.unlock() }
  176. storage.removeObject(forKey: key as NSString)
  177. keys.remove(key)
  178. }
  179. /// Removes all values in this storage.
  180. public func removeAll() {
  181. lock.lock()
  182. defer { lock.unlock() }
  183. storage.removeAllObjects()
  184. keys.removeAll()
  185. }
  186. }
  187. }
  188. extension MemoryStorage {
  189. /// Represents the configuration used in a ``MemoryStorage/Backend``.
  190. public struct Config {
  191. /// The total cost limit of the storage.
  192. ///
  193. /// This counts up the value of ``CacheCostCalculable/cacheCost``. If adding this object to the cache causes
  194. /// the cache’s total cost to rise above totalCostLimit, the cache may automatically evict objects until its
  195. /// total cost falls below this value.
  196. public var totalCostLimit: Int
  197. /// The item count limit of the memory storage.
  198. ///
  199. /// The default value is `Int.max`, which means no hard limitation of the item count.
  200. public var countLimit: Int = .max
  201. /// The ``StorageExpiration`` used in this memory storage.
  202. ///
  203. /// The default is `.seconds(300)`, which means that the memory cache will expire in 5 minutes if not accessed.
  204. public var expiration: StorageExpiration = .seconds(300)
  205. /// The time interval between the storage performing cleaning work for sweeping expired items.
  206. public var cleanInterval: TimeInterval
  207. /// Determine whether newly added items to memory cache should be purged when the app goes to the background.
  208. ///
  209. /// By default, cached items in memory will be purged as soon as the app goes to the background to ensure a
  210. /// minimal memory footprint. Enabling this prevents this behavior and keeps the items alive in the cache even
  211. /// when your app is not in the foreground.
  212. ///
  213. /// The default value is `false`. After setting it to `true`, only newly added cache objects are affected.
  214. /// Existing objects that were already in the cache while this value was `false` will still be purged when the
  215. /// app enters the background.
  216. public var keepWhenEnteringBackground: Bool = false
  217. /// Creates a configuration from a given ``MemoryStorage/Config/totalCostLimit`` value and a
  218. /// ``MemoryStorage/Config/cleanInterval``.
  219. ///
  220. /// - Parameters:
  221. /// - totalCostLimit: The total cost limit of the storage in bytes.
  222. /// - cleanInterval: The time interval between the storage performing cleaning work for sweeping expired items.
  223. /// The default is 120, which means auto eviction happens once every two minutes.
  224. ///
  225. /// > Other properties of the ``MemoryStorage/Config`` will use their default values when created.
  226. public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) {
  227. self.totalCostLimit = totalCostLimit
  228. self.cleanInterval = cleanInterval
  229. }
  230. }
  231. }
  232. extension MemoryStorage {
  233. class BackgroundKeepingStorageObject<T>: StorageObject<T>, NSDiscardableContent {
  234. var accessing = true
  235. func beginContentAccess() -> Bool {
  236. if value != nil {
  237. accessing = true
  238. } else {
  239. accessing = false
  240. }
  241. return accessing
  242. }
  243. func endContentAccess() {
  244. accessing = false
  245. }
  246. func discardContentIfPossible() {
  247. value = nil
  248. }
  249. func isContentDiscarded() -> Bool {
  250. return value == nil
  251. }
  252. }
  253. class StorageObject<T> {
  254. var value: T?
  255. let expiration: StorageExpiration
  256. private(set) var estimatedExpiration: Date
  257. init(_ value: T, expiration: StorageExpiration) {
  258. self.value = value
  259. self.expiration = expiration
  260. self.estimatedExpiration = expiration.estimatedExpirationSinceNow
  261. }
  262. func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) {
  263. switch extendingExpiration {
  264. case .none:
  265. return
  266. case .cacheTime:
  267. self.estimatedExpiration = expiration.estimatedExpirationSinceNow
  268. case .expirationTime(let expirationTime):
  269. self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow
  270. }
  271. }
  272. var isExpired: Bool {
  273. return estimatedExpiration.isPast
  274. }
  275. }
  276. }