目录
描述
Cache 并非在此领域中独一无二,但它也不是另一个提供神力的怪物库。Cache 只做缓存,但做得很好。它提供了一种良好的公共 API,带有原生的实现和出色的自定义选项。Cache 使用 Swift 4 中的 Codable
来执行序列化。
在这里阅读故事 开源故事:从 Cachable 到 Cache 中的泛型存储
主要特点
- 与 Swift 4
Codable
协作。任何遵循Codable
的内容都将通过Storage
容易地保存和加载。 - 内存和磁盘存储的混合。
- 通过
DiskConfig
和MemoryConfig
提供多种选项。 - 支持
expiry
和过期对象清理。 - 线程安全。操作可以从任何队列访问。
- 默认同步。也支持异步 API。
- 广泛覆盖单元测试和优秀的文档。
- 支持 iOS、tvOS 和 macOS。
使用方法
存储
Cache
是基于 责任链模式构建的,其中有很多处理对象,每个对象都知道如何执行 1 个任务并将其委托给下一个对象,因此您可以按照自己的喜好组合存储。
目前支持以下存储类型:
MemoryStorage
:将对象保存到内存中。DiskStorage
:将对象保存到磁盘。HybridStorage
:将对象保存到内存和磁盘,因此您可以在磁盘上获得持久化的对象,同时快速访问内存中的对象。SyncStorage
:阻塞 API,所有读写操作都安排在一个序列队列中,以同步方式执行。AsyncStorage
:非阻塞 API,操作安排在内部队列中以串行方式处理。不得同时进行读写操作。
虽然您可以选择使用这些存储,但不必这样做。因为我们还提供了一个便利的 Storage
,它使用底层的 HybridStorage
,并通过 SyncStorage
和 AsyncStorage
提供同步和异步 API。
您只需指定使用 DiskConfig
和 MemoryConfig
要配置的内容即可。默认配置就可以良好地运行,但您可以进行很多自定义。
let diskConfig = DiskConfig(name: "Floppy")
let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
let storage = try? Storage(
diskConfig: diskConfig,
memoryConfig: memoryConfig,
transformer: TransformerFactory.forCodable(ofType: User.self) // Storage<User>
)
泛型,类型安全和转换器
现在所有 Storage
都默认为泛型,因此您可以获得类型安全体验。一旦创建了一个存储,它就有了类型约束,之后您就不需要为每个操作指定类型。
如果您想更改类型,Cache
提供了 transform
函数,查找内置转换器的 Transformer
和 TransformerFactory
。
let storage: Storage<User> = ...
storage.setObject(superman, forKey: "user")
let imageStorage = storage.transformImage() // Storage<UIImage>
imageStorage.setObject(image, forKey: "image")
let stringStorage = storage.transformCodable(ofType: String.self) // Storage<String>
stringStorage.setObject("hello world", forKey: "string")
每次转换都允许您使用特定类型,然而底层的缓存机制保持不变,您正在处理的仍然是同一个 Storage
,只是使用了不同的类型注解。如果您想的话,也可以为每个类型创建不同的 Storage
。
Transformer
功能是必要的,因为它需要将对象序列化和反序列化到 Data
以实现磁盘持久化。 Cache
为 Data
、Codable
和 UIImage/NSImage
提供默认的 Transformer
可编码类型
存储库(Storage
)支持遵循 Codable 协议的任何对象。您可以 使自己的类遵循 Codable 以使其能够从存储库中保存和加载数据。
支持以下类型:
- 基本类型,如
Int
、Float
、String
、Bool
等。 - 原始类型的数组,如
[Int]
、[Float]
、[Double]
等。 - 原始类型集合,如
Set
、Set
等。 - 简单的字典,如
[String: Int]
、[String: String]
等。 日期
URL
数据
错误处理
错误处理通过 try catch
来完成。存储库通过 StorageError
抛出错误。
public enum StorageError: Error {
/// Object can not be found
case notFound
/// Object is found, but casting to requested type failed
case typeNotMatch
/// The file attributes are malformed
case malformedFileAttributes
/// Can't perform Decode
case decodingFailed
/// Can't perform Encode
case encodingFailed
/// The storage has been deallocated
case deallocated
/// Fail to perform transformation to or from Data
case transformerFail
}
可能存在由于磁盘问题或类型不匹配导致的错误,因此在存储时想处理错误,需要进行 \(\text{try catch}\) 操作。
do {
let storage = try Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)
} catch {
print(error)
}
配置
以下是您可以调整的众多配置选项之一
let diskConfig = DiskConfig(
// The name of disk storage, this will be used as folder name within directory
name: "Floppy",
// Expiry date that will be applied by default for every added object
// if it's not overridden in the `setObject(forKey:expiry:)` method
expiry: .date(Date().addingTimeInterval(2*3600)),
// Maximum size of the disk cache storage (in bytes)
maxSize: 10000,
// Where to store the disk cache. If nil, it is placed in `cachesDirectory` directory.
directory: try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,
appropriateFor: nil, create: true).appendingPathComponent("MyPreferences"),
// Data protection is used to store files in an encrypted format on disk and to decrypt them on demand
protectionType: .complete
)
let memoryConfig = MemoryConfig(
// Expiry date that will be applied by default for every added object
// if it's not overridden in the `setObject(forKey:expiry:)` method
expiry: .date(Date().addingTimeInterval(2*60)),
/// The maximum number of objects in memory the cache should hold
countLimit: 50,
/// The maximum total cost that the cache can hold before it starts evicting objects
totalCostLimit: 0
)
在 iOS 和 tvOS 上,我们还可以在 DiskConfig
上指定 protectionType
,以向应用程序容器中存储的文件增加一层安全性。有关更多信息,请参阅 FileProtectionType
同步 API
存储库默认是同步的,并且是 线程安全
的,您可以从任何队列访问它。所有同步函数都受制于 StorageAware
协议。
// Save to storage
try? storage.setObject(10, forKey: "score")
try? storage.setObject("Oslo", forKey: "my favorite city", expiry: .never)
try? storage.setObject(["alert", "sounds", "badge"], forKey: "notifications")
try? storage.setObject(data, forKey: "a bunch of bytes")
try? storage.setObject(authorizeURL, forKey: "authorization URL")
// Load from storage
let score = try? storage.object(forKey: "score")
let favoriteCharacter = try? storage.object(forKey: "my favorite city")
// Check if an object exists
let hasFavoriteCharacter = try? storage.existsObject(forKey: "my favorite city")
// Remove an object in storage
try? storage.removeObject(forKey: "my favorite city")
// Remove all objects
try? storage.removeAll()
// Remove expired objects
try? storage.removeExpiredObjects()
入口
有时候您想要获取对象及其过期信息和元数据。您可以使用 入口
let entry = try? storage.entry(forKey: "my favorite city")
print(entry?.object)
print(entry?.expiry)
print(entry?.meta)
meta
可能包含文件信息,如果对象是从磁盘存储中检索的。
自定义编码
Codable
对简单的字典如 [String: Int]
,[String: String]
,... 有效。它不能用于 [String: Any]
,因为 Any
不遵守 Codable
协议,这将在运行时引发 致命错误
。所以当您从后端响应中获取 json 时,您需要将其转换为您的自定义 Codable
对象,并将其保存到 Storage
中。
struct User: Codable {
let firstName: String
let lastName: String
}
let user = User(fistName: "John", lastName: "Snow")
try? storage.setObject(user, forKey: "character")
异步API
在异步方式中,您处理 Result
而不是 try catch
,因为结果将在稍后时间交付,这样就不会阻塞当前调用队列。在完成块中,您要么有 value
或者 error
。
您通过 storage.async
访问异步API,它是线程安全的,您可以根据需要顺序使用同步和异步API。所有异步函数都受限于 AsyncStorageAware
协议。
storage.async.setObject("Oslo", forKey: "my favorite city") { result in
switch result {
case .value:
print("saved successfully")
case .error(let error):
print(error)
}
}
storage.async.object(forKey: "my favorite city") { result in
switch result {
case .value(let city):
print("my favorite city is \(city)")
case .error(let error):
print(error)
}
}
storage.async.existsObject(forKey: "my favorite city") { result in
if case .value(let exists) = result, exists {
print("I have a favorite city")
}
}
storage.async.removeAll() { result in
switch result {
case .value:
print("removal completes")
case .error(let error):
print(error)
}
}
storage.async.removeExpiredObjects() { result in
switch result {
case .value:
print("removal completes")
case .error(let error):
print(error)
}
}
过期时间
默认情况下,所有已保存的对象都与其在 DiskConfig
或 MemoryConfig
中指定的过期时间相同。您可以通过为 setObject
指定 expiry
来覆盖特定对象的过期时间。
// Default expiry date from configuration will be applied to the item
try? storage.setObject("This is a string", forKey: "string")
// A given expiry date will be applied to the item
try? storage.setObject(
"This is a string",
forKey: "string"
expiry: .date(Date().addingTimeInterval(2 * 3600))
)
// Clear expired objects
storage.removeExpiredObjects()
观察
存储 允许您观察缓存层的变化,无论是在存储层面还是在键层面。API 允许您传递任何对象作为观察者,同时还可以传递一个观察闭包。当弱捕获的观察者被回收时,观察闭包将自动移除。
存储观察
// Add observer
let token = storage.addStorageObserver(self) { observer, storage, change in
switch change {
case .add(let key):
print("Added \(key)")
case .remove(let key):
print("Removed \(key)")
case .removeAll:
print("Removed all")
case .removeExpired:
print("Removed expired")
}
}
// Remove observer
token.cancel()
// Remove all observers
storage.removeAllStorageObservers()
键观察
let key = "user1"
let token = storage.addObserver(self, forKey: key) { observer, storage, change in
switch change {
case .edit(let before, let after):
print("Changed object for \(key) from \(String(describing: before)) to \(after)")
case .remove:
print("Removed \(key)")
}
}
// Remove observer by token
token.cancel()
// Remove observer for key
storage.removeObserver(forKey: key)
// Remove all observers
storage.removeAllKeyObservers()
处理 JSON 响应
大多数情况下,我们的用例是从后端获取一些 JSON,显示它同时将 JSON 保存到存储中供将来使用。如果您使用类似 Alamofire 或 Malibu 的库,您可以以字典、字符串或数据的形式获取 JSON。
存储
可以持久化 String
或 Data
。您甚至可以使用 JSONArrayWrapper
和 JSONDictionaryWrapper
将 JSON 保存到 存储
中,但我们更喜欢持久化强类型对象,因为这些都是您用于显示 UI 的对象。此外,如果 JSON 数据不能转换为强类型对象,那么保存它的意义何在?
您可以使用这些扩展在 JSONDecoder
上解码 JSON 字典、字符串或数据到对象。
let user = JSONDecoder.decode(jsonString, to: User.self)
let cities = JSONDecoder.decode(jsonDictionary, to: [City].self)
let dragons = JSONDecoder.decode(jsonData, to: [Dragon].self)
这是使用 Alamofire
执行对象转换和保存的方式
Alamofire.request("https://gameofthrones.org/mostFavoriteCharacter").responseString { response in
do {
let user = try JSONDecoder.decode(response.result.value, to: User.self)
try storage.setObject(user, forKey: "most favorite character")
} catch {
print(error)
}
}
关于图片怎么办
如果您想将图片加载到 UIImageView
或 NSImageView
中,那么我们还为您准备了一个礼物。它被称为 Imaginary,并使用 Cache
来简化您处理远程图片时的工作。
安装
CocoaPods
缓存可通过CocoaPods获取。要安装,只需在Podfile中添加以下行
pod 'Cache'
Carthage
缓存也通过Carthage获取。要安装,只需在Cartfile中写入以下内容
github "hyperoslo/Cache"
您还需要在copy-frameworks脚本中添加SwiftHash.framework
。
作者
贡献
我们非常欢迎您为缓存做出贡献,请查看CONTRIBUTING文件获取更多信息。
许可协议
缓存可在MIT许可下使用。更多信息请参阅LICENSE文件。