Сохранить содержимое NSCache на диск - PullRequest
14 голосов
/ 28 декабря 2010

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

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

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

Спасибо!

Ответы [ 4 ]

13 голосов
/ 28 декабря 2010

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

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

NSCache не предназначено для обеспечения устойчивости.

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

9 голосов
/ 07 ноября 2013

использовать TMCache (https://github.com/tumblr/TMCache). Это похоже на NSCache, но с сохранением и очисткой кэша. Написано командой Tumblr.

4 голосов
/ 26 марта 2017

Иногда может быть удобнее не иметь дело с Базовыми данными, а просто сохранять содержимое кэша на диск.Вы можете достичь этого с помощью 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 - она ​​не предоставляет способ извлечения ключей для хранимых объектов.Есть три способа обойти это:

  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)
    }
}
0 голосов
/ 26 марта 2017

Вы должны попробовать 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 :(")
}
...