I'm writing an app that needs to keep an in-memory cache of a bunch of objects, but that doesn't get out of hand so I'm planning on using NSCache to store it all. Looks like it will take care of purging and such for me, which is fantastic.
I'd also like to persist the cache between launches, so I need to write the cache data to disk. Is there an easy way to save the NSCache contents to a plist or something? Are there perhaps better ways to accomplish this using something other than NSCache?
This app will be on the iPhone, so I'll need only classes that are available in iOS 4+ and not just OS X.
Thanks!
I'm writing an app that needs to keep an in-memory cache of a bunch of objects, but that doesn't get out of hand so I'm planning on using NSCache to store it all. Looks like it will take care of purging and such for me, which is fantastic. I'd also like to persist the cache between launches, so I need to write the cache data to disk. Is there an easy way to save the NSCache contents to a plist or something? Are there perhaps better ways to accomplish this using something other than NSCache?
You pretty much just described exactly what CoreData does; persistency of object graphs with purging and pruning capabilities.
NSCache
is not designed to assist with persistency.
Given that you suggested persisting to a plist format, using Core Data instead isn't that big of a conceptual difference.
use TMCache (https://github.com/tumblr/TMCache). It's like NSCache but with persistence and cache purging. Written by the Tumblr team.
Sometimes it may be more convenient not to deal with Core Data and just to save cache content to disk. You can achieve this with NSKeyedArchiver
and UserDefaults
(I'm using Swift 3.0.2 in code examples below).
First let's abstract from NSCache
and imagine that we want to be able to persist any cache that conforms to protocol:
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
associated type has to be Hashable
because that's requirement for Set
type parameter.
Next we have to implement NSCoding
for Cache
using helper class 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)
}
}
Here:
C
is type that conforms toCache
.C.Key
associated type has to conform to:- Swift
CustomStringConvertible
protocol to be convertible toString
becauseNSCoder.encode(forKey:)
method acceptsString
for key parameter. - Swift
ExpressibleByStringLiteral
protocol to convert[String]
back toSet<Key>
- Swift
- We need to convert
Set<Key>
to[String]
and store it toNSCoder
withkeys
key because there is no way to extract during decoding fromNSCoder
keys that were used when encoding objects. But there may be situation when we also have entry in cache with keykeys
so to distinguish cache keys from specialkeys
key we prefix cache keys with_
. C.Value
associated type has to conform toNSCodingConvertible
protocol to getNSCoding
instances from the values stored in cache:protocol NSCodingConvertible { associatedtype Coding: NSCoding var coding: Coding { get } }
Value.Coding
has to conform toValueProvider
protocol because you need to get values back fromNSCoding
instances:protocol ValueProvider { associatedtype Value var value: Value { get } }
C.Value.Coding.Value
andC.Value
have to be equivalent because the value from which we getNSCoding
instance when encoding must have the same type as value that we get back fromNSCoding
when decoding.CB
is a type that conforms toBuilder
protocol and helps to create cache instance ofC
type:protocol Builder { associatedtype Value init() func build() -> Value }
Next let's make NSCache
conform to Cache
protocol. Here we have a problem. NSCache
has the same issue as NSCoder
does - it does not provide the way to extract keys for stored objects. There are three ways to workaround this:
Wrap
NSCache
with custom type which will hold keysSet
and use it everywhere instead ofNSCache
: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) } }
If you still need to pass
NSCache
somewhere then you can try to extend it in Objective-C doing the same thing as I did above withBetterCache
.Use some other cache implementation.
Now you have type that conforms to Cache
protocol and you are ready to use it.
Let's define type Book
which instances we will store in cache and NSCoding
for that type:
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)
}
}
Some typealiases for better readability:
typealias BookCache = BetterCache<StringKey, Book>
typealias BookCacheCoding = CacheCoding<BookCache, BookCacheBuilder>
And builder that will help us to instantiate Cache
instance:
class BookCacheBuilder: Builder {
required init() {
}
func build() -> BookCache {
return BookCache()
}
}
Test it:
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)
}
}
You should try AwesomeCache. Its main features:
- written in Swift
- uses on on-disk caching
- backed by NSCache for maximum performance and support for expiry of single objects
Example:
do {
let cache = try Cache<NSString>(name: "awesomeCache")
cache["name"] = "Alex"
let name = cache["name"]
cache["name"] = nil
} catch _ {
print("Something went wrong :(")
}
来源:https://stackoverflow.com/questions/4542707/save-nscache-contents-to-disk