Сохранить содержимое NSCache на диск

Я пишу приложение, которое должно хранить в кеше в памяти кучу объектов, но это не выходит из-под контроля, поэтому я планирую использовать NSCache для хранения всего этого. Похоже, он позаботится о чистке и прочее для меня, что просто фантастика.

Я также хотел бы сохранить кеш между запусками, поэтому мне нужно записать данные кеша на диск. Есть ли простой способ сохранить содержимое NSCache в plist или что-то? Возможно, есть лучшие способы сделать это, используя что-то кроме NSCache?

Это приложение будет на iPhone, поэтому мне понадобятся только классы, доступные в iOS 4+, а не только OS X.

Спасибо!

4 ответа

Решение

Я пишу приложение, которое должно хранить в кеше в памяти кучу объектов, но это не выходит из-под контроля, поэтому я планирую использовать NSCache для хранения всего этого. Похоже, он позаботится о чистке и прочее для меня, что просто фантастика. Я также хотел бы сохранить кеш между запусками, поэтому мне нужно записать данные кеша на диск. Есть ли простой способ сохранить содержимое NSCache в plist или что-то? Возможно, есть лучшие способы сделать это, используя что-то кроме NSCache?

Вы в значительной степени только что описали, что именно делает CoreData; постоянство графов объектов с возможностями очистки и сокращения.

NSCache не предназначен, чтобы помочь с постоянством.

Учитывая, что вы предложили сохранить формат plist, использование Core Data вместо концептуальной разницы не так уж и велико.

Используйте TMCache ( https://github.com/tumblr/TMCache). Это как NSCache, но с постоянством и очисткой кеша. Автор команды Tumblr.

Иногда может быть удобнее не иметь дело с 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 связанный тип должен соответствовать:
    • стриж CustomStringConvertible протокол для преобразования в String так как NSCoder.encode(forKey:) метод принимает String для ключевого параметра.
    • стриж 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 делает - он не предоставляет способ извлечения ключей для хранимых объектов. Есть три способа обойти это:

  1. Заворачивать 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)
        }
    }
    
  2. Если вам все еще нужно пройти NSCache тогда вы можете попытаться расширить его в Objective-C, выполнив то же самое, что я сделал выше BetterCache,

  3. Используйте другую реализацию кеша.

Теперь у вас есть тип, который соответствует 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)
    }
}

Вы должны попробовать 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 :(")
}
Другие вопросы по тегам