开源故事:从缓存中的可缓存到通用存储

目录

  • 快取
  • 可携带
  • 异步
  • JSON格式
  • Swift 4中的可编码
  • 责任链
  • 什么是TypeWrapper?
  • StorageAware协议
  • 同步与异步
  • 但是图像不符合Codable
  • 解决
  • 怎么样?
  • 数据可转换怎么样?
  • 数据生产者呢?
  • 通用存储
  • 转换类型
  • 存储感知
  • 变压器
  • 转换功能
  • 从这往哪儿走

我们从事开源已有一段时间了,您可能已经在GitHub上遇到了我们的一些工作或阅读了一些故事。 我们并没有尝试重新发明轮子,但是有许多我们需要专门用于工作流的组件,或者需要针对正在构建的应用程序进行自定义的组件。 因此,我们构建了许多框架和应用程序。 当我们在生产应用程序中使用它们时,我们认为与世界分享它们可能是一个好主意。 这是双赢的局面,因为我们回馈了社区,同时获得了很多反馈和建议。 作为一个小型的iOS团队,全职进行客户项目,同时尝试腾出一些空闲时间来从事开源工作是非常具有挑战性的。

开源就是建立抽象。 通过分离职责并创建可重用的框架,我们可以充分了解Swift的知识,并掌握有关正在使用的API的一些精妙的细节。 但是我们从未告诉过我们如何做事。 因此,将会有一系列我们的开源故事,详细介绍我们工作背后的技术方面,只要有开源经验即可。

首先,让我们谈谈缓存,一种持久化对象的框架。 在这里,我们将学习如何开发API以支持Swift语言和iOS,tvOS平台的新功能,同时确保它们足够灵活和可维护。

缓存并没有声称在该区域是唯一的,但是它并不是另一个为您提供神力的怪物库。 除了缓存,它什么也没做,但它做得很好。 它提供了良好的公共API,具有现成的实现方式和巨大的自定义可能性

在iOS平台上可以有许多用于缓存的解决方案,例如SQLite,CoreData或Realm或其他第三库。 我们想要从Cache中获得的是一种简单的方法,可以将一些JSON数据存储到磁盘,到期管理和我们熟悉的API。

从用户的角度来看,我想使用键可靠地保存和加载对象,并具有同步或异步执行此操作的能力。 这是我们旨在实现的API。

在第一个版本中,由于对象应该被序列化并反序列化为用于磁盘存储的Data ,因此我们引入了Cachable协议。 我们还使大多数原始类型都符合Cachable,因此用户不必自己执行此操作。 对于内存存储,我们在NSCache使用NSCache ,因此对象将按原样保存。

缓存基于具有前缓存和后缓存的概念。 对前端缓存的请求应减少时间和内存消耗(此处默认使用NSCache )。 正面和背面缓存之间的区别在于,背面缓存用于超出应用程序生命周期的内容。 看到它更像是一种方便的方法,用于存储应在应用程序启动期间持续存在的用户信息。 磁盘缓存是这里最可靠的选择

HybridCache具有具有Cachable类型约束的泛型函数,因此对于所有Cachable一致性它都是类型安全的

甚至对于自定义类型和UIImage

异步

默认情况下,缓存是同步的,这意味着所有方法都处于阻塞状态。 为了以异步方式访问缓存,有一个方便的异步方式导致AsyncHybridCache 。 所有组件在CacheManager共享相同的CacheManager ,因此所有对象都相同,只是不同的接口。

JSON格式

当我们大多数时间处理JSON时,有一个JSON枚举封装了顶级JSON对象或JSON数组,并使用JSONSerialization转换为Data以获得可Cachable一致性

Swift 4最重要的功能之一是Codable 。 符合Codable类型可以映射到JSON或从JSON映射。 JSONSerializationJSONSerialization ,但是我们需要关心的是使我们的类型符合Codable ,并确保我们的模型属性与JSON数据中的那些键匹配。 仅仅声明一个模型,从JSON解码并将其持久保存到Cache有多酷? 因此,在Cache 4.0中,我们重构了公共API以更好地支持Codable。

鉴于有传言说NSCache将重命名为Cache ,并且为避免结构Cache窃取Cache名称空间,我们将Cache类重命名为Storage 。 通过更好地封装磁盘和内存存储的Config对象,可以轻松声明自己的存储。

责任链

缓存是基于“责任链”模式构建的,其中有许多处理对象,每个对象都知道如何执行一项任务,并将其委托给下一个任务。 但这只是实现细节。 您需要了解的只是存储,它可以保存和加载可编码对象。

设计存储时要牢记责任链模式,其中每个Storage充当处理对象。 我们仅处理Storage ,但是引擎盖下有一条链

Storage -> SyncStorage -> TypeWrapperStorage -> HybridStorage -> DiskStorage & MemoryStorage

每个处理对象都包含定义其可以处理的命令对象类型的逻辑。 其余的传递给链中的下一个处理对象

Storage负责根据传入的配置构造内部StoragesSyncStorage处理用于异步访问的串行队列管理。 HybridStorage协调MemoryStorageDiskStorage ……

什么是TypeWrapper?

Int, String, Bool等原始类型都符合Codable ,因此在编译器满意的情况下调用storage.save(“a string”, forKey: “myKey”) 。 但是,由于我们在JSONEncoder使用JSONEncoderJSONDecoder ,因此简单地使用原始类型可能会导致运行时异常,例如“将顶级T编码为数字JSON片段”或“希望对T进行解码但找到了字典”。是PrimitiveStorage的原因

在这里,我们需要捕获这些错误,并在出现错误的情况下使用PrimitiveWrapper ,以便始终有一个可以与JSON数据进行序列化的顶级对象。

其中PrimitiveWrapper是具有Codable约束的简单泛型结构

后来我虽然说如果我们总是可以执行换行的话,代码将会更少,这导致了我的Add TypeWrapperStorage拉取请求。 这样,代码很容易推理,但是开销很大。

StorageAware协议

为了使所有Storage易于“链接”,它们均符合StorageAware协议,该协议定义了Storage必须支持的一组最低功能。

这样做的StorageAware是,我们可以利用Swift中的协议扩展为StorageAware符合者提供默认实现。 从Entry信息中,我们可以推断出object以及它是否存在。

所有功能都有Codable约束,因此我们拥有非常安全的类型体验。

同步与异步

默认情况下, Storage是同步的。 您可能已经注意到,所有同步功能都被标记为具有错误类型StorageError throws 。 我们设计了try catch用于同步,而Result用于异步。 对于异步,我们无法try catch因为结果将在以后交付。 因此,我们使用completion闭包来异步调用有关结果的调用方。

就像我们对StorageAware所做的那样,所有异步Storage遵循AsyncStorageAware协议。 为了确保不会同时发生读写操作,我们使用串行DispatchQueue按顺序分派操作

由于我们希望在同一Storage上同时支持同步和异步操作,因此我们最初在SyncStorageAsyncStorage之间共享1个串行队列,因此无论执行了多少操作,它们都处于安全的顺序。 但是由于我们还使用serialQueue.sync来获取SyncStorage阻塞行为,并使用serialQueue.sync来获取SyncStorage行为, AsyncStorage这可能导致死锁! 因此,最终我们为SyncStorageAsyncStorage使用了不同的DispatchQueue ,这可以在死锁之间进行AsyncStorage ,如果用户调用同步和异步交换,则可能会遇到严重的节访问冲突。

但是图像不符合Codable

UIImageNSImage不符合Codable 。 由于我们将API设计为专门支持Codable ,因此处理图像非常棘手。 仅仅使UIImage符合Codable是行不通的,这样做是没有意义的。

本质上,对于图像,用户应将它们另存为Data到磁盘,然后将其文件URL保留在Storage 。 但是为了支持统一的Codable体验,我们引入了ImageWrapper 。 如果现有的类型(如UIImage不符合Codable ,则包装器可以

然后,您要做的就是将UIImage包装在ImageWrapper ,并获得与Codable相同的API支持。

但是,这个漂亮的API需要注意一些开销。 可能没什么大不了的,但是对于像Imaginary这样依赖于Cache的库而言,要大量存储和获取图像可能是个大问题。 这是对象在磁盘上的方式。

  {“对象”:{“图像”:” \ / 9j \ / 4AAQSkZJRgABAQAASABIAAD \ / 4QBYRXhpZ… 

顶级JSON对象和图像到字符串的转换会导致开销。 理想情况下,图像应仅保存为Data

怎么样?

支持UIImage一种方法是删除Codable约束,并使用Any ,如下所示

这不会编译,因为Codable正常工作,必须在编译时知道类型。 这是因为协议不符合自身,请阅读使用JSON编码器将Codable作为类型编码变量,而协议不符合自身? 有关更多详细说明。

因此,我们不采用这种方法。

数据可转换怎么样?

UIImageCodable之间的区别在于如何将它们转换为Data ,我们需要Data可转换对象进行磁盘存储。 因此,我们只需要封装此要求,从DataConvertible协议开始,并使UIImageCodable符合该要求

但是,这并不容易。 我们无法使现有协议Codable符合我们的协议。

这种方法不可行,我们不赞成。

数据生产者呢?

Codable协议扩展根本不起作用。 让我们回到类DataProducer对象合成中,它具有通用的Codable约束,同时存储UIImageCodable 。 因此,当要求它生成Data ,它将检查它是否具有CodableUIImage

这种方法是可行的,并且编译良好。 对于磁盘存储,我们调用toData产生数据,对于内存存储,我们可以将内部对象设置为NSCache 。 但是,不需要将DataProducer指定为包装器通常不会使用户满意。 我们需要一种不同的方法。

从Cache 5.0开始,我们解决了这些开销和纯UIImage支持,同时允许Cache灵活且易于自定义。

转换类型

同时支持UIImageCodable更好方法是使用通用Storage ,在其中可以转换类型。 这样,我们可以根据需要进行转换以支持其他自定义类型。

这样, Storage是极其类型安全的,您可以一次保存和加载相同类型的对象。 但是Storage可以是可转换的,底层存储机制保持不变,只是公共API支持不同的类型。 这是在用户希望将CodableUIImage都保存到同一存储中的情况。 但是,我们仍然建议为每种类型使用不同的存储。

存储感知

所有Storage仍然符合StorageAware因此我们在StorageAware协议扩展中有一些不错的默认实现。 请注意,由于Storage是通用的,因此我们的StorageAware现在具有associatedtype T以反映Storage的通用值类型。

由于我们无法将具有通用约束的协议定义为变量,因此我们可以像以前一样自由地链接所有Storage 。 现在,在Cache我们有了固定的依赖关系,这意味着SyncStorage现在明确指定了HybridStorage 。 但是,我们确实将所有Storage公开给了公众,因此您可以按照自己的方式进行组合。 但是默认Storage结构在大多数情况下应该很好。

变压器

当用户指定Storage类型时,他们还需要指定一个Transformer 。 它是包含2个函数fromDatatoData数据结构。 对于DiskStorage ,这是必需的,因为我们必须支持一种通用类型可转换为Data

由于CodableDataUIImage是我们保存并加载到Storage的最常见格式,因此我们提供了默认的TransformerFactory

转换功能

要将Storage转换为新的类型,我们只需将Storage内部的所有内部对象移动到新的Storage ,它们都是引用类型,因此会产生开销。 我们还需要为新类型指定Transformer 。 每个存储都有transform功能

您绝对应该看一下有关存储转换功能多么强大的测试。 每当转换存储时,都会将其约束为新类型,因此所有操作都是类型安全的,但是所有对象均保存在同一位置。

新的通用Storage API类型安全且灵活。 它还减少了所有解决方法的开销。 我们在想象中的图像获取器中使用了它来提高性能。

唯一的“常数”是“变化”。 Apple平台和Swift编程语言的发展速度比您想象的要快,我们的框架可以利用所有新功能是件好事。 希望您在此重构过程中发现Cache和我们的故事有用。

最后但并非最不重要的一点,感谢使Cache成为现实的所有贡献者。 你比真棒❤️

下个故事再见。