我使用 Core Data 已经有三年的时间了,虽然至今也不能算是完全掌握,但基本上可以做到熟练使用,很少会犯原则性的错误了。当前,如何让 Core Data 融入流行的应用架构体系,在 SwiftUI、TCA、Unit Tests、Preview 等环境下更加顺畅地工作已成为我的主要困扰和研究方向。我将通过几篇文章来介绍近半年来在这方面的一些想法、收获、体会及实践,也希望能够与有类似困惑的朋友进行更多的探讨。
廉颇老矣尚能饭否?
Core Data 是一个拥有悠久历史的框架。如果仅从苹果在 2005 年发布 MacOS X Tigger 中第一次集成了 Core Data 框架算起,Core Data 至今只有十余年的时间,但考虑到其很大一部分设计都继承自 Next 于 1994 年推出的 EOF( Enterprise Objects Framework )框架,如此算来,其核心设计理念已经诞生了接近三十年。在 Core Data 框架的代码中,至今仍到处充斥着具有历史感的 NS
前缀。
与当前 Core Data 主流的使用场景不同,EOF 被集成在应用程序服务器 WebObjects 中,在电子商务的早期,吸引了不少大公司的使用,客户包括 BBC、Dell、Disney、GE、Merrill Lynch 等。直到前些年,WebObjects 仍为苹果的 Apple Store 和 iTunes Store 提供动力。如此,便不难理解为什么与其他流行的移动持久化方案不同,Core Data 并不过分追求数据的访问效率,稳定才是其最关注的点,这在很多开发者中早已 形成共识。
或许是设计理念十分超前并且实现得已经足够完美( 低情商:近些年苹果投入度较低 ),最近五六年中,苹果在不需要对核心代码做太多调整的情况下,便为 Core Data 增加了如下的新功能:
-
NSPersistentContainer
协调器、持久化存储、托管上下文包装的官方实现。几乎无需调整任何核心代码。
-
持久化历史跟踪
近期最大的改动。在持久化存储上增加了更多 Triger 的操作,并在协调器上提供了响应变化的 API
-
数据的批量操作
允许开发者跳过上下文,直接从协调器上对持久化存储进行批量操作
-
Core Data with CloudKit
几乎不需对核心代码做调整,新增了 NSPersistentCloudKitContainer ,在协调器上附加了一个用于网络同步的模块。
-
async/await 支持
提供了新的 perform 方法的实现
尽管开发者中近期流传着( 或幻想着 )苹果会推出全新的框架取代 Core Data 的言论,但只要认真了解并研究 Core Data 的历史和代码便可以分析出新框架出现的可能性十分的低。一方面,其优秀的架构设计仍可满足未来添加新功能的需求;另一方面,替换一个拥有如此悠久历史且以稳定性著称的框架需要极大的勇气。因此,开发者可能会在未来很长的一段时间中继续使用这套框架。
严格来说,排除掉不易学、不好掌握这个缺点,在一个理想的环境中,Core Data 无论从稳定性、开发效率、可扩展性等方面来说都相当的优秀( 网络同步不稳定不是 Core Data 的问题 ),在管理对象图、对象生命周期以及数据持久化方面仍是苹果生态的最佳选择。
不过这并非意味着 Core Data 能够完全适应如今的开发环境。虽然它仍然拥有超前的头脑、强壮的内脏,但外貌实在太陈旧了,已很难与新框架和新的开发流程匹配。假如我们可以为它创建一个新的外貌,或许可以让它重焕青春,再战十年。
你的荣耀,我的烦恼
有趣的是,造成 Core Data 与新框架、新开发流程不融洽的大多因素都是 Core Data 引以为傲的一些特点或优势。
数据结构谁做主
Core Data 的核心是对象图管理,持久化功能只是其一个附带功能。相较于其他框架,Core Data 对关系的描述和处理能力是其核心竞争力。或许是为了便于描述复杂的关系逻辑,开发者在创建数据结构前,通常需要在 Xcode 的数据模型编辑器中创建实体描述( 支持使用代码直接来定义,但较少会采用此种方式 ),然后使用自动或手动的方式生成对应的 NSManagedObject 定义代码。如此一来会出现如下问题:
- 为了保持与 Objectiv-C 的兼容性( Core Data 的内部数据仍采用 Objective-C 实现 ),开发者在数据模型编辑器中,仅能用有限的数据类型来描述属性。这使得开发者在定义一个新的数据结构( 对应 Core Data 的实体 )时,无法在第一时间用最适合 Swift 语言风格的方式进行思考和描述,不自觉地便受制于模型编辑器的表述能力。
- 在使用了数据网络同步的情况下( Core Data with CloudKit),由于无法在产品上线后修改实体或属性名称( [只增不减不改原则]( https://fatbobman.com/zh/posts/coredatawithcloudkit-4/#更新数据模型 ) ),无论原有的实体、属性、关系名称定义得多么不合理,开发者也只能承受。随着版本的不断更迭,这些不合适的命名会充斥在代码各处,让人欲哭无泪。
- 很难在第一时间进入业务流程的开发状态。当将托管对象作为数据描述的类型后,开发者往往最初编写的代码都是有关于 Core Data Stack 方面的。在应用的开发过程中,对数据定义的任何调整都需要经过层层处理( 模型编辑器、对应的 NSManamgedObject 定义、Stack 中的相关代码 ),严重影响了开发的效率。
总而言之,一旦在应用中使用了 Core Data,开发者很难在开发的初始阶段摆脱它的阴影。从导入 Core Data 的那一刻起,便对开发者的创造力、直觉、热情产生了负面的影响。
R 0 值超高的托管机制
Core Data 的托管机制自 EOF 时期便已经存在。该机制让 Core Data 将来自底层的数据源暴露为持久对象的托管图( 内存数据对象 ),并通过托管上下文对对象图进行修改和跟踪。托管机制提供的数据惰性加载能力可以帮助开发者在读取效率和内存占用之间取得平衡。可以说,拥有托管机制是 Core Data 长期以来的一个引以为傲的特性。
但托管机制意味着,开发者在进行任意操作前首先要搭建符合要求的托管环境。操作托管对象必须首先创建托管对象上下文。而让上下文可以工作的前提是创建托管协调器和持久化存储。
除了创建托管环境所需操作繁杂外,托管环境在某些场合下的运行稳定性并不可靠。事实上,Core Data 的托管环境已经是当下导致 SwiftUI 预览失败的主要原因之一。另外,对托管环境的准备和重置也会拖慢 Unit Tests 的速度,影响开发者编写单元测试的意愿。由此一来,会严重打击开发者在应用中采用模块式( SPM )开发的积极性。
如果说奥密克戎 BA. 4/5 的 R 0 值为 18.6 ,那么托管机制对于应用中涉及托管对象的代码的基本繁殖数就是 ∞ ,一旦沾上便甩不掉。
线程绑定与 Sendable
虽然 Core Data 的托管对象并非线程安全的,但只要严格遵守使用约定( 只在创建托管对象的托管上下文中使用 ),在 Core Data 中进行多线程开发是很安全的。尽管有些开发者认为在 Core Data 中进行多线程有些烦琐,但又不得不承认相较于其他类似的框架,使用 Core Data 进行多线程开发,稳定性是很有保障的。
随着 Swift 5.5 在异步和并发方面能力的提升,开发者会自觉不自觉的在代码中使用到新的异步或并发机制。例如,TCA 的 Reducer 目前正朝 Global Actor 方向演进( 也就是 Reducer 将不再运行于主线程上 )。为了避免出现线程安全问题,让数据符合 Sendable 协议是有效的手段。
很显然,托管对象并不具备符合 Sendable 协议的基础。如何让 Core Data 与使用新并行机制的框架进行配合,同样是摆在开发者面前的一个新课题。
我向往的使用方式
尽管有些贪心,但我仍希望能做到鱼和熊掌兼而得之。我们将通过几篇文章一起来探讨,试图实现如下目标:
- 将 Core Data 对数据定义过程中( 尤其是开发的初期 ) 的影响降至最低
- 将数据源切换至 Core Data 后,无需修改当前的代码
- 在预览、单元测试阶段不再受托管环境的困扰,可方便对代码实行模块化管理
- 仍保留 Core Data 的数据惰性加载机制,避免造成内存的过多占用
- 兼容新的并行机制,找寻 Senable 的最大公约数
- 用最少的代码实现上述目标,避免增加系统的不稳定性
下文介绍
在下篇文章中,我们将首先从数据( 对应 Core Data 的实体、属性 )的定义谈起,尝试通过泛型、类型擦除等方式从定义中移除托管环境。
由于一直没有为这个系列的文章想好恰当的题目,便暂且临时采用了 “SwiftUI 与 Core Data” 这个名称。如果你有什么好的建议欢迎告诉我。