Cache 并不声称是该领域唯一的解决方案,但它也不是一个赋予您强大能力的巨型库。 它只做缓存,但做得很好。 它提供了一个良好的公共 API,具有开箱即用的实现和强大的自定义可能性。 Cache
利用 Swift 4 中的 Codable
执行序列化。
在此阅读故事:开源故事:从 Cachable 到 Cache 中的通用存储
Codable
。任何符合 Codable
的内容都可以轻松地被 Storage
保存和加载。DiskConfig
和 MemoryConfig
提供多种选项。expiry
(过期时间)和过期对象的清理。Cache
基于 责任链模式 构建,其中有许多处理对象,每个对象都知道如何完成一项任务并委托给下一个对象,因此您可以按照自己喜欢的方式组合存储。
目前支持以下存储:
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<String, User>
)
所有 Storage
现在默认都是泛型的,因此您可以获得类型安全的体验。 创建存储后,它会有一个类型约束,您以后无需为每个操作指定类型。
如果您想更改类型,Cache
提供了 transform
函数,请查找内置转换器的 Transformer
和 TransformerFactory
。
let storage: Storage<String, User> = ...
storage.setObject(superman, forKey: "user")
let imageStorage = storage.transformImage() // Storage<String, UIImage>
imageStorage.setObject(image, forKey: "image")
let stringStorage = storage.transformCodable(ofType: String.self) // Storage<String, String>
stringStorage.setObject("hello world", forKey: "string")
每次转换都允许您使用特定类型,但是底层缓存机制保持不变,您使用的是相同的 Storage
,只是使用了不同的类型注释。 如果需要,您还可以为每种类型创建不同的 Storage
。
Transformer
是必要的,因为需要将对象序列化和反序列化为 Data
以实现磁盘持久性。 Cache
为 Data
、Codable
和 UIImage/NSImage
提供了默认的 Transformer
。
Storage
支持任何符合 Codable 协议的对象。 您可以 使您自己的内容符合 Codable,以便可以从 Storage
保存和加载。
支持的类型有:
Int
、Float
、String
、Bool
等。[Int]
、[Float]
、[Double]
等。Set<String>
、Set<Int>
等。[String: Int]
、[String: String]
等。日期
URL
数据
错误处理通过 try catch
完成。 Storage
抛出 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
}
可能由于磁盘问题或从存储加载时类型不匹配而导致错误,因此如果要处理错误,您需要执行 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。
Storage
默认是同步的,并且是 thread safe
(线程安全)的,您可以从任何队列访问它。 所有同步函数都受到 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.objectExists(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()
有时您想获取对象及其过期信息和元数据。 您可以使用 Entry
。
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
,它会在运行时引发 fatal error
。 因此,当您从后端响应中获取 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")
以 async
方式,您处理的是 Result
而不是 try catch
,因为结果会在稍后的时间传递,以避免阻塞当前调用队列。 在完成块中,您要么有 value
,要么有 error
。
您可以通过 storage.async
访问异步 API,它也是线程安全的,您可以按任何顺序使用同步和异步 API。 所有异步函数都受到 AsyncStorageAware
协议的约束。
storage.async.setObject("Oslo", forKey: "my favorite city") { result in
switch result {
case .success:
print("saved successfully")
case .failure(let error):
print(error)
}
}
storage.async.object(forKey: "my favorite city") { result in
switch result {
case .success(let city):
print("my favorite city is \(city)")
case .failure(let error):
print(error)
}
}
storage.async.objectExists(forKey: "my favorite city") { result in
if case .success(let exists) = result, exists {
print("I have a favorite city")
}
}
storage.async.removeAll() { result in
switch result {
case .success:
print("removal completes")
case .failure(let error):
print(error)
}
}
storage.async.removeExpiredObjects() { result in
switch result {
case .success:
print("removal completes")
case .failure(let error):
print(error)
}
}
do {
try await storage.async.setObject("Oslo", forKey: "my favorite city")
print("saved successfully")
} catch {
print(error)
}
do {
let city = try await storage.async.object(forKey: "my favorite city")
print("my favorite city is \(city)")
} catch {
print(error)
}
do {
let exists = try await storage.async.objectExists(forKey: "my favorite city")
if exists {
print("I have a favorite city")
}
} catch {}
do {
try await storage.async.remoeAll()
print("removal completes")
} catch {
print(error)
}
do {
try await storage.async.removeExpiredObjects()
print("removal completes")
} catch {
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 保存到存储以供将来使用。 如果您使用的是像 Alamofire 或 Malibu 这样的库,您通常会获得字典、字符串或数据形式的 json。
Storage
可以持久化 String
或 Data
。 您甚至可以使用 JSONArrayWrapper
和 JSONDictionaryWrapper
将 json 保存到 Storage
,但我们更喜欢持久化强类型对象,因为这些是您将在 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.success, to: User.self)
try storage.setObject(user, forKey: "most favorite character")
} catch {
print(error)
}
}
如果您想将图像加载到 UIImageView
或 NSImageView
中,那么我们还有一个不错的礼物给您。 它叫做 Imaginary,并在底层使用 Cache
,让您在处理远程图像时更轻松。
Cache 可通过 CocoaPods 获得。 要安装或更新它,请将以下行添加到您的 Podfile 中:
pod 'Cache', :git => 'https://github.com/hyperoslo/Cache.git'
Cache 也可以通过 Carthage 获得。 要安装,只需写入您的 Cartfile:
github "hyperoslo/Cache"
您还需要在 copy-frameworks 脚本中添加 SwiftHash.framework
。
我们很乐意您为 Cache 做出贡献,请查看 CONTRIBUTING 文件以获取更多信息。
Cache 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。