Cache

CI Status Version Carthage Compatible License Platform Documentation Swift

目录

描述

Cache Icon

Cache 并不声称是该领域唯一的解决方案,但它也不是一个赋予您强大能力的巨型库。 它只做缓存,但做得很好。 它提供了一个良好的公共 API,具有开箱即用的实现和强大的自定义可能性。 Cache 利用 Swift 4 中的 Codable 执行序列化。

在此阅读故事:开源故事:从 Cachable 到 Cache 中的通用存储

主要特性

用法

存储

Cache 基于 责任链模式 构建,其中有许多处理对象,每个对象都知道如何完成一项任务并委托给下一个对象,因此您可以按照自己喜欢的方式组合存储。

目前支持以下存储:

虽然您可以酌情使用这些存储,但您不必这样做。 因为我们还提供了一个方便的 Storage,它在底层使用 HybridStorage,同时通过 SyncStorageAsyncStorage 公开同步和异步 API。

您只需使用 DiskConfigMemoryConfig 指定您想要的配置。 默认配置就可以正常工作,但您可以自定义很多。

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 函数,请查找内置转换器的 TransformerTransformerFactory

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 以实现磁盘持久性。 CacheDataCodableUIImage/NSImage 提供了默认的 Transformer

Codable 类型

Storage 支持任何符合 Codable 协议的对象。 您可以 使您自己的内容符合 Codable,以便可以从 Storage 保存和加载。

支持的类型有:

错误处理

错误处理通过 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

同步 API

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)

有时您想获取对象及其过期信息和元数据。 您可以使用 Entry

let entry = try? storage.entry(forKey: "my favorite city")
print(entry?.object)
print(entry?.expiry)
print(entry?.meta)

如果对象是从磁盘存储中获取的,则 meta 可能包含文件信息。

自定义 Codable

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")

异步 API

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)
  }
}

Swift 并发

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)
}

过期日期

默认情况下,所有保存的对象都具有与您在 DiskConfigMemoryConfig 中指定的过期时间相同的过期时间。 您可以通过为 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 保存到存储以供将来使用。 如果您使用的是像 AlamofireMalibu 这样的库,您通常会获得字典、字符串或数据形式的 json。

Storage 可以持久化 StringData。 您甚至可以使用 JSONArrayWrapperJSONDictionaryWrapper 将 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)
  }
}

图片呢?

如果您想将图像加载到 UIImageViewNSImageView 中,那么我们还有一个不错的礼物给您。 它叫做 Imaginary,并在底层使用 Cache,让您在处理远程图像时更轻松。

安装

Cocoapods

Cache 可通过 CocoaPods 获得。 要安装或更新它,请将以下行添加到您的 Podfile 中:

pod 'Cache', :git => 'https://github.com/hyperoslo/Cache.git'

Carthage

Cache 也可以通过 Carthage 获得。 要安装,只需写入您的 Cartfile:

github "hyperoslo/Cache"

您还需要在 copy-frameworks 脚本中添加 SwiftHash.framework

作者

贡献

我们很乐意您为 Cache 做出贡献,请查看 CONTRIBUTING 文件以获取更多信息。

许可证

Cache 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。