Сохранить содержимое NSCache на диск
Я пишу приложение, которое должно хранить кеш в памяти кучи объектов, но это не выходит из-под контроля, поэтому я планирую использовать NSCache для его хранения. Похоже, что он позаботится о чистке и для меня, что фантастично.
Я также хотел бы сохранить кеш между запусками, поэтому мне нужно записать данные кэша на диск. Есть ли простой способ сохранить содержимое NSCache в plist или что-то еще? Существуют ли, возможно, лучшие способы достижения этого, используя что-то другое, чем NSCache?
Это приложение будет на iPhone, поэтому мне нужны только классы, доступные в iOS 4+, а не только OS X.
Спасибо!
Ответы
Ответ 1
Я пишу приложение, которое нужно сохранить в кеше в памяти пучка объектов, но это не выходит из так что я планирую использовать NSCache чтобы сохранить все это. Похоже, это будет позаботьтесь о чистке и для меня, который является фантастическим. Я также хотел бы сохранить кеш между запусками, поэтому мне нужно написать данные кэша на диск. Есть простой способ сохранить содержимое NSCache к plist или что-то еще? Здесь возможно, лучшие способы достижения этого используя что-то, кроме NSCache?
Вы в значительной степени просто описали, что делает CoreData; постоянство графиков объектов с возможностями очистки и обрезки.
NSCache
не предназначен для обеспечения стойкости.
Учитывая, что вы предложили сохранить формат plist, использование Core Data вместо этого отличается от концептуальной разницы.
Ответ 2
используйте TMCache (https://github.com/tumblr/TMCache). Это похоже на NSCache, но с сохранением и очисткой кэша. Написано командой Tumblr.
Ответ 3
Иногда может быть удобнее не иметь дело с Core Data и просто сохранять содержимое кеша на диск. Вы можете достичь этого с помощью NSKeyedArchiver
и UserDefaults
(я использую Swift 3.0.2 в примерах кода ниже).
Сначала позвольте отвлечься от NSCache
и представьте, что мы хотим сохранить любой кеш, соответствующий протоколу:
protocol Cache {
associatedtype Key: Hashable
associatedtype Value
var keys: Set<Key> { get }
func set(value: Value, forKey key: Key)
func value(forKey key: Key) -> Value?
func removeValue(forKey key: Key)
}
extension Cache {
subscript(index: Key) -> Value? {
get {
return value(forKey: index)
}
set {
if let v = newValue {
set(value: v, forKey: index)
} else {
removeValue(forKey: index)
}
}
}
}
Key
связанный тип должен быть Hashable
, поскольку это требование для параметра типа Set
.
Далее мы должны реализовать NSCoding
для Cache
с помощью вспомогательного класса CacheCoding
:
private let keysKey = "keys"
private let keyPrefix = "_"
class CacheCoding<C: Cache, CB: Builder>: NSObject, NSCoding
where
C.Key: CustomStringConvertible & ExpressibleByStringLiteral,
C.Key.StringLiteralType == String,
C.Value: NSCodingConvertible,
C.Value.Coding: ValueProvider,
C.Value.Coding.Value == C.Value,
CB.Value == C {
let cache: C
init(cache: C) {
self.cache = cache
}
required convenience init?(coder decoder: NSCoder) {
if let keys = decoder.decodeObject(forKey: keysKey) as? [String] {
var cache = CB().build()
for key in keys {
if let coding = decoder.decodeObject(forKey: keyPrefix + (key as String)) as? C.Value.Coding {
cache[C.Key(stringLiteral: key)] = coding.value
}
}
self.init(cache: cache)
} else {
return nil
}
}
func encode(with coder: NSCoder) {
for key in cache.keys {
if let value = cache[key] {
coder.encode(value.coding, forKey: keyPrefix + String(describing: key))
}
}
coder.encode(cache.keys.map({ String(describing: $0) }), forKey: keysKey)
}
}
Здесь:
-
C
- тип, соответствующий Cache
.
-
C.Key
связанный тип должен соответствовать:
- Swift
CustomStringConvertible
для преобразования в String
, потому что метод NSCoder.encode(forKey:)
принимает String
для ключевого параметра.
- Swift
ExpressibleByStringLiteral
для преобразования [String]
обратно в Set<Key>
- Нам нужно преобразовать
Set<Key>
в [String]
и сохранить его в NSCoder
с помощью keys
, потому что нет способа извлечь во время декодирования из NSCoder
ключей, которые использовались при кодировании объектов. Но может возникнуть ситуация, когда у нас также есть запись в кеше с ключом keys
, поэтому для различения ключей кеша из специального ключа keys
мы используем кеширование ключей с _
.
-
C.Value
связанный тип должен соответствовать протоколу NSCodingConvertible
, чтобы получить экземпляры NSCoding
из значений, хранящихся в кеше:
protocol NSCodingConvertible {
associatedtype Coding: NSCoding
var coding: Coding { get }
}
-
Value.Coding
должен соответствовать протоколу ValueProvider
, потому что вам нужно вернуть значения из экземпляров NSCoding
:
protocol ValueProvider {
associatedtype Value
var value: Value { get }
}
-
C.Value.Coding.Value
и C.Value
должны быть эквивалентными, потому что значение, из которого мы получаем экземпляр NSCoding
, когда кодирование должно иметь тот же тип, что и значение, которое мы получаем от NSCoding
при декодировании.
-
CB
- это тип, который соответствует протоколу Builder
и помогает создать экземпляр кеша типа C
:
protocol Builder {
associatedtype Value
init()
func build() -> Value
}
Далее сделаем NSCache
совместимым с протоколом Cache
. Здесь у нас есть проблема. NSCache
имеет ту же проблему, что и NSCoder
- это не обеспечивает способ извлечения ключей для сохраненных объектов. Существует три способа обхода этого пути:
-
Оберните NSCache
специальным типом, который будет содержать клавиши Set
и использовать его везде, а не NSCache
:
class BetterCache<K: AnyObject & Hashable, V: AnyObject>: Cache {
private let nsCache = NSCache<K, V>()
private(set) var keys = Set<K>()
func set(value: V, forKey key: K) {
keys.insert(key)
nsCache.setObject(value, forKey: key)
}
func value(forKey key: K) -> V? {
let value = nsCache.object(forKey: key)
if value == nil {
keys.remove(key)
}
return value
}
func removeValue(forKey key: K) {
return nsCache.removeObject(forKey: key)
}
}
-
Если вам все равно нужно передать NSCache
где-нибудь, тогда вы можете попробовать расширить его в Objective-C, делая то же самое, что и я, с помощью BetterCache
.
-
Используйте другую реализацию кэша.
Теперь у вас есть тип, который соответствует протоколу Cache
, и вы готовы его использовать.
Пусть определите тип Book
, какие экземпляры мы будем хранить в кеше и NSCoding
для этого типа:
class Book {
let title: String
init(title: String) {
self.title = title
}
}
class BookCoding: NSObject, NSCoding, ValueProvider {
let value: Book
required init(value: Book) {
self.value = value
}
required convenience init?(coder decoder: NSCoder) {
guard let title = decoder.decodeObject(forKey: "title") as? String else {
return nil
}
print("My Favorite Book")
self.init(value: Book(title: title))
}
func encode(with coder: NSCoder) {
coder.encode(value.title, forKey: "title")
}
}
extension Book: NSCodingConvertible {
var coding: BookCoding {
return BookCoding(value: self)
}
}
Некоторые типы для лучшей читаемости:
typealias BookCache = BetterCache<StringKey, Book>
typealias BookCacheCoding = CacheCoding<BookCache, BookCacheBuilder>
И построитель, который поможет нам создать экземпляр экземпляра Cache
:
class BookCacheBuilder: Builder {
required init() {
}
func build() -> BookCache {
return BookCache()
}
}
Проверьте это:
let cacheKey = "Cache"
let bookKey: StringKey = "My Favorite Book"
func test() {
var cache = BookCache()
cache[bookKey] = Book(title: "Lord of the Rings")
let userDefaults = UserDefaults()
let data = NSKeyedArchiver.archivedData(withRootObject: BookCacheCoding(cache: cache))
userDefaults.set(data, forKey: cacheKey)
userDefaults.synchronize()
if let data = userDefaults.data(forKey: cacheKey),
let cache = (NSKeyedUnarchiver.unarchiveObject(with: data) as? BookCacheCoding)?.cache,
let book = cache.value(forKey: bookKey) {
print(book.title)
}
}
Ответ 4
Вам следует попробовать AwesomeCache. Его основные возможности:
- написан в Swift
- использует кеширование на диске
- поддерживаемый NSCache для максимальной производительности и поддержки для истечения срока действия отдельных объектов
Пример:
do {
let cache = try Cache<NSString>(name: "awesomeCache")
cache["name"] = "Alex"
let name = cache["name"]
cache["name"] = nil
} catch _ {
print("Something went wrong :(")
}