Core Data 改革:实现 SwiftData 般的优雅并发操作
source link: https://fatbobman.com/zh/posts/core-data-reform-achieving-elegant-concurrency-operations-like-swiftdata/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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 等新框架,我依然鼓励开发者深入了解并学习它们,让这些新的设计理念丰富你的知识库。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK