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.
 
 
 
 

231 lines
9.0 KiB

//
// 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 a set of conception related to storage which stores a certain type of value in memory.
/// This is a namespace for the memory storage types. A `Backend` with a certain `Config` will be used to describe the
/// storage. See these composed types for more information.
public enum MemoryStorage {
/// Represents a storage which stores a certain type of value in memory. It provides fast access,
/// but limited storing size. The stored value type needs to conform to `CacheCostCalculable`,
/// and its `cacheCost` will be used to determine the cost of size for the cache item.
///
/// You can config a `MemoryStorage.Backend` in its initializer by passing a `MemoryStorage.Config` value.
/// or modifying the `config` property after it being created. The backend of `MemoryStorage` has
/// upper limitation on cost size in memory and item count. All items in the storage has an expiration
/// date. When retrieved, if the target item is already expired, it will be recognized as it does not
/// exist in the storage. The `MemoryStorage` also contains a scheduled self clean task, to evict expired
/// items from memory.
public class Backend<T: CacheCostCalculable> {
let storage = NSCache<NSString, StorageObject<T>>()
var keys = Set<String>()
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 {
didSet {
storage.totalCostLimit = config.totalCostLimit
storage.countLimit = config.countLimit
}
}
/// Creates a `MemoryStorage` with a given `config`.
///
/// - Parameter config: The config used to create the storage. It determines the max size limitation,
/// default expiration setting and more.
public init(config: Config) {
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 }
self.removeExpired()
}
}
func removeExpired() {
lock.lock()
defer { lock.unlock() }
for key in keys {
let nsKey = key as NSString
guard let object = storage.object(forKey: nsKey) else {
keys.remove(key)
continue
}
if object.estimatedExpiration.isPast {
storage.removeObject(forKey: nsKey)
keys.remove(key)
}
}
}
// Storing in memory will not throw. It is just for meeting protocol requirement and
// forwarding to no throwing method.
func store(
value: T,
forKey key: String,
expiration: StorageExpiration? = nil) throws
{
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(value, key: key, expiration: expiration)
storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
keys.insert(key)
}
// Use this when you actually access the memory cached item.
// This will extend the expired data for the accessed item.
func value(forKey key: String) throws -> T? {
return value(forKey: key, extendingExpiration: true)
}
func value(forKey key: String, extendingExpiration: Bool) -> T? {
guard let object = storage.object(forKey: key as NSString) else {
return nil
}
if object.expired {
return nil
}
if extendingExpiration { object.extendExpiration() }
return object.value
}
func isCached(forKey key: String) -> Bool {
guard let _ = value(forKey: key, extendingExpiration: false) else {
return false
}
return true
}
func remove(forKey key: String) throws {
lock.lock()
defer { lock.unlock() }
storage.removeObject(forKey: key as NSString)
keys.remove(key)
}
func removeAll() throws {
lock.lock()
defer { lock.unlock() }
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)
}
}
}
}
}
extension MemoryStorage {
/// Represents the config used in a `MemoryStorage`.
public struct Config {
/// Total cost limit of the storage in bytes.
public var totalCostLimit: Int
/// The item count limit of the memory storage.
public var countLimit: Int = .max
/// The `StorageExpiration` used in this memory storage. Default is `.seconds(300)`,
/// means that the memory cache would expire in 5 minutes.
public var expiration: StorageExpiration = .seconds(300)
/// The time interval between the storage do clean work for swiping expired items.
public let cleanInterval: TimeInterval
/// Creates a config from a given `totalCostLimit` value.
///
/// - Parameters:
/// - totalCostLimit: Total cost limit of the storage in bytes.
/// - cleanInterval: The time interval between the storage do clean work for swiping expired items.
/// Default is 120, means the auto eviction happens once per two minutes.
///
/// - Note:
/// Other members of `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 StorageObject<T> {
let value: T
let expiration: StorageExpiration
let key: String
private(set) var estimatedExpiration: Date
init(_ value: T, key: String, expiration: StorageExpiration) {
self.value = value
self.key = key
self.expiration = expiration
self.estimatedExpiration = expiration.estimatedExpirationSinceNow
}
func extendExpiration() {
self.estimatedExpiration = expiration.estimatedExpirationSinceNow
}
var expired: Bool {
return estimatedExpiration.isPast
}
}
}