开源故事:从缓存中的可缓存到通用存储
目录
- 快取
- 可携带
- 异步
- 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
映射。 JSONSerialization
有JSONSerialization
,但是我们需要关心的是使我们的类型符合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
负责根据传入的配置构造内部Storages
。 SyncStorage
处理用于异步访问的串行队列管理。 HybridStorage
协调MemoryStorage
和DiskStorage
……
什么是TypeWrapper?
像Int, String, Bool
等原始类型都符合Codable
,因此在编译器满意的情况下调用storage.save(“a string”, forKey: “myKey”)
。 但是,由于我们在JSONEncoder
使用JSONEncoder
和JSONDecoder
,因此简单地使用原始类型可能会导致运行时异常,例如“将顶级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
上同时支持同步和异步操作,因此我们最初在SyncStorage
和AsyncStorage
之间共享1个串行队列,因此无论执行了多少操作,它们都处于安全的顺序。 但是由于我们还使用serialQueue.sync
来获取SyncStorage
阻塞行为,并使用serialQueue.sync
来获取SyncStorage
行为, AsyncStorage
这可能导致死锁! 因此,最终我们为SyncStorage
和AsyncStorage
使用了不同的DispatchQueue
,这可以在死锁之间进行AsyncStorage
,如果用户调用同步和异步交换,则可能会遇到严重的节访问冲突。
但是图像不符合Codable
UIImage
和NSImage
不符合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作为类型编码变量,而协议不符合自身? 有关更多详细说明。
因此,我们不采用这种方法。
数据可转换怎么样?
UIImage
和Codable
之间的区别在于如何将它们转换为Data
,我们需要Data
可转换对象进行磁盘存储。 因此,我们只需要封装此要求,从DataConvertible
协议开始,并使UIImage
和Codable
符合该要求
但是,这并不容易。 我们无法使现有协议Codable
符合我们的协议。
这种方法不可行,我们不赞成。
数据生产者呢?
Codable
协议扩展根本不起作用。 让我们回到类DataProducer
对象合成中,它具有通用的Codable
约束,同时存储UIImage
或Codable
。 因此,当要求它生成Data
,它将检查它是否具有Codable
或UIImage
。
这种方法是可行的,并且编译良好。 对于磁盘存储,我们调用toData
产生数据,对于内存存储,我们可以将内部对象设置为NSCache
。 但是,不需要将DataProducer
指定为包装器通常不会使用户满意。 我们需要一种不同的方法。
从Cache 5.0开始,我们解决了这些开销和纯UIImage
支持,同时允许Cache
灵活且易于自定义。
转换类型
同时支持UIImage
和Codable
更好方法是使用通用Storage
,在其中可以转换类型。 这样,我们可以根据需要进行转换以支持其他自定义类型。
这样, Storage
是极其类型安全的,您可以一次保存和加载相同类型的对象。 但是Storage
可以是可转换的,底层存储机制保持不变,只是公共API支持不同的类型。 这是在用户希望将Codable
和UIImage
都保存到同一存储中的情况。 但是,我们仍然建议为每种类型使用不同的存储。
存储感知
所有Storage
仍然符合StorageAware
因此我们在StorageAware
协议扩展中有一些不错的默认实现。 请注意,由于Storage
是通用的,因此我们的StorageAware
现在具有associatedtype T
以反映Storage
的通用值类型。
由于我们无法将具有通用约束的协议定义为变量,因此我们可以像以前一样自由地链接所有Storage
。 现在,在Cache
我们有了固定的依赖关系,这意味着SyncStorage
现在明确指定了HybridStorage
。 但是,我们确实将所有Storage
公开给了公众,因此您可以按照自己的方式进行组合。 但是默认Storage
结构在大多数情况下应该很好。
变压器
当用户指定Storage
类型时,他们还需要指定一个Transformer
。 它是包含2个函数fromData
和toData
数据结构。 对于DiskStorage
,这是必需的,因为我们必须支持一种通用类型可转换为Data
。
由于Codable
, Data
和UIImage
是我们保存并加载到Storage
的最常见格式,因此我们提供了默认的TransformerFactory
转换功能
要将Storage
转换为新的类型,我们只需将Storage
内部的所有内部对象移动到新的Storage
,它们都是引用类型,因此会产生开销。 我们还需要为新类型指定Transformer
。 每个存储都有transform
功能
您绝对应该看一下有关存储转换功能多么强大的测试。 每当转换存储时,都会将其约束为新类型,因此所有操作都是类型安全的,但是所有对象均保存在同一位置。
新的通用Storage
API类型安全且灵活。 它还减少了所有解决方法的开销。 我们在想象中的图像获取器中使用了它来提高性能。
唯一的“常数”是“变化”。 Apple平台和Swift编程语言的发展速度比您想象的要快,我们的框架可以利用所有新功能是件好事。 希望您在此重构过程中发现Cache
和我们的故事有用。
最后但并非最不重要的一点,感谢使Cache成为现实的所有贡献者。 你比真棒❤️
下个故事再见。