将NSCache内容保存到磁盘

21

我正在编写一个应用程序,需要在内存中保留一堆对象的缓存,但是为了避免混乱,我计划使用NSCache来存储所有内容。看起来它会负责清理等工作,这太好了。

我还想将缓存持久化到磁盘上,以便在启动时恢复缓存数据。有没有一种简单的方法将NSCache的内容保存为plist或其他格式?也许有更好的方法可以使用不同于NSCache的东西来完成这个任务吗?

这个应用程序将在iPhone上运行,所以我只需要使用iOS 4+可用的类库,而不是仅限于OS X。

谢谢!

4个回答

13
我正在编写一个需要在内存中缓存大量对象的应用程序,但是为了防止出现问题,我计划使用NSCache来存储所有内容。看起来,它会自动帮我处理清除等问题,这太棒了。
我还想在启动之间保留缓存,因此需要将缓存数据写入磁盘。有什么简单的方法可以将NSCache内容保存到plist文件或其他格式吗?是否有更好的方法可以使用NSCache以外的其他东西来实现这一点?
你几乎完全描述了CoreData所做的内容;它可以持久化对象图并具有清除和修剪功能。
NSCache没有设计用于帮助持久性。
考虑到您建议将内容持久化到plist格式,使用CoreData与之相比概念上并没有太大的区别。

1
我几乎在我创建的每个数据库应用程序中都使用了 Core Data,但它似乎并不是最好的选择。我正在使用缓存来存储 API 结果,在启动时和加载已经加载过的东西时保持应用程序的快速响应。我担心 core data 数据库会失控并不断增长。看起来 core data 并不适合用于缓存“临时”数据。我需要的持久性基本上是我需要的内存缓存功能的次要功能,这就是为什么我倾向于使用 NSCache 的原因。 - Cory Imdieke
是的 - 我能理解你的困境。NSCoding 并不是很难实现 - 你总可以走这条路。然后问题就变成了何时编写/更新缓存的持久版本。根据我的经验,很容易走上一条以所有复杂性重新发明持久性轮子的道路。当然,最好的表现应用程序是首先发布的那个。;) - bbum
@CoryImdieke,我们现在也面临着同样的情况,我打算使用NSCache。只是想知道你选择了哪个解决方案? - Koolala
这是一段时间以前的事情了,但我记得我们基本上使用了一个NSDictionary,并将所有内容写入一个Plist文件中,然后编写了自己的缓存清理程序,在必要时清理字典。我们基本上构建了自己的NSCache风格的对象,但添加了持久性。 - Cory Imdieke
最好的答案是现在使用CoreData或Realm。CoreData是最标准的,而Realm更易于学习。 - Philip Fung

9

1
很遗憾,它已不再得到积极维护 :/ - manicaesar
目前最佳答案是使用CoreData或者Realm。 CoreData是最常用的,而Realm更容易学习。 - Philip Fung

4
有时不直接处理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类型参数的要求。

接下来,我们需要使用辅助类CacheCoding实现CacheNSCoding

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 is type that conforms to Cache.
  • C.Key associated type has to conform to:
    • Swift CustomStringConvertible protocol to be convertible to String because NSCoder.encode(forKey:) method accepts String for key parameter.
    • Swift ExpressibleByStringLiteral protocol to convert [String] back to Set<Key>
  • We need to convert Set<Key> to [String] and store it to NSCoder with keys key because there is no way to extract during decoding from NSCoder keys that were used when encoding objects. But there may be situation when we also have entry in cache with key keysso to distinguish cache keys from special keys key we prefix cache keys with _.
  • C.Value associated type has to conform to NSCodingConvertible protocol to get NSCoding instances from the values stored in cache:

    protocol NSCodingConvertible {
        associatedtype Coding: NSCoding
    
        var coding: Coding { get }
    }
    
  • Value.Coding has to conform to ValueProvider protocol because you need to get values back from NSCoding instances:

    protocol ValueProvider {
        associatedtype Value
    
        var value: Value { get }
    }
    
  • C.Value.Coding.Value and C.Value have to be equivalent because the value from which we get NSCoding instance when encoding must have the same type as value that we get back from NSCoding when decoding.

  • CB is a type that conforms to Builder protocol and helps to create cache instance of C type:

    protocol Builder {
        associatedtype Value
    
        init()
    
        func build() -> Value
    }
    
接下来让我们使NSCache符合Cache协议。这里我们有一个问题。NSCacheNSCoder一样存在一个问题 - 它不提供提取存储对象键的方法。有三种解决方法:
  1. Wrap NSCache with custom type which will hold keys Set and use it everywhere instead of 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. 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 with BetterCache.

  3. Use some other cache implementation.

现在你有符合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)
    }
}

谢谢您详细说明。StringKey是如何定义的?它抱怨了String、NSString和StaticString。 - VaporwareWolf

0

你应该尝试使用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 :(")
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接