SwiftData,作为 Core Data 的后继者,引入了众多创新和现代化的设计思想。尽管它已经推出一段时间,但许多开发者还未在他们的项目中采用。这种状况部分是因为 SwiftData 对操作系统版本的要求较高,另一方面,由于 SwiftData 在某些功能方面还不够成熟,即便操作系统版本符合要求,开发者也可能因为功能限制而选择继续使用 Core Data。我们是否能将 SwiftData 中的一些卓越设计理念和巧妙实现,融合到 Core Data 的实际使用中呢?本文旨在探讨如何在 Core Data 中引入类似 SwiftData 的优雅和安全的并发操作,以实现一个 @ModelActor
的 Core Data 版本。
perform
VS @ModelActor
尽管在理论上,只需要遵循一个简单原则即可在 Core Data 中安全地进行并发操作:托管对象应仅在其绑定的托管对象上下文及相应线程中被操作。然而,要遵守这个规则,完全取决于开发者的耐心和经验,而编译器在这方面无法提供帮助。因此,在 Core Data 的并发代码实践中,广泛使用基于上下文的 perform
方法,这种做法既繁琐又难以控制。
SwiftData 克服了这些障碍。通过采用 Swift 的现代并发模型,开发者可以避开 perform
,将数据操作逻辑封装在一个 Actor
中。此外,SwiftData 还引入了 @ModelActor
宏,允许 Actor
在特定线程中执行,为开发者提供了一种优雅、安全、高效的并发操作方式。
@ModelActor
actor DataHandler {
func updateItem(identifier: PersistentIdentifier, timestamp: Date) throws {
guard let item = self[identifier, as: Item.self] else {
throw MyError.objectNotExist
}
item.timestamp = timestamp
try modelContext.save()
}
}
推荐阅读 关于 Core Data 并发编程的几点提示 了解更多关于 Core Data 并发操作的建议。同时,浏览 SwiftData 中的并发编程 可以深入了解 SwiftData 在并发操作方面的创新之处。
自定义 Actor 执行者
自 Swift 5.5 引入新并发模型以来,Actor
已经成为开发者执行串行操作的首选机制。然而,这种新的并发设计有意识的模糊了代码的实际运行方式和细节,使得很长一段时间,开发者都无法决定 Actor
的具体执行位置( 也就是所在的线程 )。
遵循 Core Data 并发操作的基本原则,所有对托管对象的操作都必须在其所属上下文的线程上执行。这个限制意味着我们无法直接将 Actor
模型应用于 Core Data 的并发操作中。
然而,Swift 社区通过 SE-392 提案,提出了自定义 Actor
执行者的概念,并在 Swift 5.9 中实现了这一功能。SwiftData 利用这一新特性,为开发者提供了一种全新的并发开发体验。
这意味着,我们现在可以为 Actor
创建一个 Executor
,用它来替换 Actor
默认的任务调度机制。
创建自定义 Executor
在构建自定义 Actor
执行者之前,了解一些基本概念是必要的:
- Executors 协议:一个基本的执行器,不提供任何调度顺序的保证,可以并行或串行地执行提交的任务。
- SerialExecutor 协议:一个串行执行器,符合
Executors
协议。它保证任务的互斥执行,也就是说一次只能执行一个任务。这个协议被 Actor 用来实现他们的串行执行语义。 - UnownedSerialExecutor:一个优化过的
SerialExecutor
引用类型,它为 Swift 并发运行时提供了高效的执行器引用机制,从而避免不必要的开销。这有助于提升 Swift 并发编程的性能。 - ExecutorJob:一个可以被执行的任务类型,支持
Sendable
协议且不可复制(@noncopyable
)。执行者在需要执行任务时,会调用ExecutorJob.runSynchronously(on:)
方法,该方法会消耗掉ExecutorJob
实例,并在指定的执行者上同步执行任务。 - UnownedExecutorJob:作为
ExecutorJob
的补充类型,它是可复制的,使得任务存储和传递变得更加容易。
为 Actor
构建自定义执行者时,大致步骤如下:
- 声明一个遵循
SerialExecutor
协议的类型。 - 在其内部实现一个可以进行串行操作的机制。
- 在
enqueue
方法中,将ExecutorJob
转换为UnownedExecutorJob
并提交给串行机制执行。
具体的实现示例如下所示:
public final class CustomExecutor: SerialExecutor {
// 串行工具
private let serialQueue: DispatchQueue
public init(serialQueue: DispatchQueue) {
self.serialQueue = serialQueue
}
public func enqueue(_ job: consuming ExecutorJob) {
// 转换 ExecutorJob 为 UnownedJob
let unownedJob = UnownedJob(job)
let unownedExecutor = asUnownedSerialExecutor()
// 在串行队列中执行任务
serialQueue.async {
unownedJob.runSynchronously(on: unownedExecutor)
}
}
// 转换自身为 UnownedSerialExecutor
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
对于 Core Data 的应用场景,我们可以直接利用托管对象上下文的 perform
方法作为串行操作的工具。经过适当调整后的实现如下:
public final class NSModelObjectContextExecutor: @unchecked Sendable, SerialExecutor {
public final let context: NSManagedObjectContext
public init(context: NSManagedObjectContext) {
self.context = context
}
public func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(job)
let unownedExecutor = asUnownedSerialExecutor()
context.perform {
unownedJob.runSynchronously(on: unownedExecutor)
}
}
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
通过将此执行者应用于 Actor
,我们能够确保该 Actor
内的所有操作(构造函数除外)均在其托管对象上下文对应的线程上执行。
构建 Actor
在 Actor
中引入自定义执行者非常直接,仅需声明一个 unownedExecutor
属性。编译器识别到 Actor
包含此属性后,将通过此执行者进行任务调度。
public nonisolated var unownedExecutor: UnownedSerialExecutor
借此,我们便能够实现一个类似于 SwiftData,用于处理 Core Data 并发操作的 Actor
。
actor DataHandler {
public nonisolated let modelExecutor: CoreDataEvolution.NSModelObjectContextExecutor
public nonisolated let modelContainer: CoreData.NSPersistentContainer
public init(container: CoreData.NSPersistentContainer) {
// 初始化私有上下文
let context = container.newBackgroundContext()
// 实例化自定义执行者
modelExecutor = CoreDataEvolution.NSModelObjectContextExecutor(context: context)
modelContainer = container
}
// 获取 Actor 所需的自定义执行者(UnownedSerialExecutor)
public nonisolated var unownedExecutor: UnownedSerialExecutor {
modelExecutor.asUnownedSerialExecutor()
}
// 用于 Actor 中数据操作的托管对象上下文
public var modelContext: NSManagedObjectContext {
modelExecutor.context
}
// 实现类似于 SwiftData 的托管对象访问机制
public subscript<T>(id: NSManagedObjectID, as _: T.Type) -> T? where T: NSManagedObject {
try? modelContext.existingObject(with: id) as? T
}
}
实现 @NSModelActor
宏:简化 Core Data 并发操作
在 SwiftData 中,开发者仅需使用 @ModelActor
宏即可自动完成上文中的繁琐设置。为了提供相似的开发体验,我们引入了针对 Core Data 的 @NSModelActor
宏。
首先,我们对 Actor 的声明进行抽象化,引入 NSModelActor
协议:
public protocol NSModelActor: Actor {
/// 为 NSModelActor 指定的 NSPersistentContainer
nonisolated var modelContainer: NSPersistentContainer { get }
/// 协调模型 actor 访问的执行者。
nonisolated var modelExecutor: NSModelObjectContextExecutor { get }
}
extension NSModelActor {
/// 模型 actor 的执行者的优化、非拥有引用。
public nonisolated var unownedExecutor: UnownedSerialExecutor {
modelExecutor.asUnownedSerialExecutor()
}
/// 序列化运行在模型 actor 上代码的上下文。
public var modelContext: NSManagedObjectContext {
modelExecutor.context
}
/// 根据指定的标识符返回模型,向下转型为适当的类。
public subscript<T>(id: NSManagedObjectID, as _: T.Type) -> T? where T: NSManagedObject {
try? modelContext.existingObject(with: id) as? T
}
}
随后,声明 @NSModelActor
宏,该宏需符合 ExtensionMacro
和 MemberMacro
协议:
@attached(member, names: named(modelExecutor), named(modelContainer), named(init))
@attached(extension, conformances: NSModelActor)
public macro NSModelActor() = #externalMacro(module: "CoreDataEvolutionMacrosPlugin", type: "NSModelActorMacro")
由于无需对原始代码进行任何特殊处理,因此宏的实现相对简单:
public enum NSModelActorMacro {}
extension NSModelActorMacro: ExtensionMacro {
public static func expansion(of _: SwiftSyntax.AttributeSyntax, attachedTo _: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo _: [SwiftSyntax.TypeSyntax], in _: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
// 生成符合 NSModelActor 协议的扩展代码
let decl: DeclSyntax =
"""
extension \(type.trimmed): CoreDataEvolution.NSModelActor {}
"""
guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else {
return []
}
return [extensionDecl]
}
}
extension NSModelActorMacro: MemberMacro {
public static func expansion(of _: AttributeSyntax, providingMembersOf _: some DeclGroupSyntax, conformingTo _: [TypeSyntax], in _: some MacroExpansionContext) throws -> [DeclSyntax] {
// 添加构造器及所需属性
[
"""
public nonisolated let modelExecutor: CoreDataEvolution.NSModelObjectContextExecutor
public nonisolated let modelContainer: CoreData.NSPersistentContainer
public init(container: CoreData.NSPersistentContainer) {
let context = container.newBackgroundContext()
modelExecutor = CoreDataEvolution.NSModelObjectContextExecutor(context: context)
modelContainer = container
}
""",
]
}
}
现在,开发者就可以在 Core Data 中享受与 SwiftData 一样的优雅、安全的并发操作了!
SerialExecutor
和ExecutorJob
仅支持在 iOS 17、macOS 14 及以上系统使用,目前还不能在较低版本的系统中应用。希望苹果公司能够像之前对并发模型所做的那样,将这部分 API 兼容到更低版本的系统,让更多开发者能够受益。
为便于广大开发者使用,我已将上述代码整合至 CoreDataEvolution 库中,并期待将 SwiftData 中获得的灵感逐步在此库中实现,同时也欢迎更多开发者的参与。
结语
在 Let’s VisionOS 2024 活动中,我进行了题为 新框架、新思维:探索 Observation 与 SwiftData 的演讲,核心旨在于强调:虽然新框架旨在解决旧框架的问题,我们却不应受旧有经验和习惯的束缚。应以开放心态,从新角度学习和应用这些工具,把采纳新框架视为向更安全、更现代化转型的良机。
探索新框架的价值不仅在于应用它们的新 API,更重要的是,通过它们的设计理念启发我们优化传统框架的开发方式。因此,哪怕在长时间内你还不能使用 SwiftData、SwiftUI、Observation 等新框架,我依然鼓励开发者深入了解并学习它们,让这些新的设计理念丰富你的知识库。