Core Data:杂项与最佳实践
Core Data: Sundries and maxims
2020年6月24日
一句话判断
Core Data 团队罕见地在 WWDC 上做了一场”知识补完” Session,涵盖并发、迁移、性能调优等长期困扰开发者的杂项——如果你还在用 Core Data,这是必看内容。
这场 Session 讲了什么
这场 Session 的标题”杂项与格言”(Sundries and Maxims)就说明了它的定位——不是介绍新 API,而是系统性地梳理 Core Data 使用中那些容易踩坑的地方。
内容分为几个部分:首先是 NSPersistentContainer 的并发模型,如何正确使用 perform() 和 performAndWait() 在不同队列中操作 Managed Object。然后是数据迁移(Migration)的最佳实践,包括轻量级迁移的局限性和自定义迁移策略。接着是性能相关的主题:Faulting 机制的工作原理、批量操作的优化、以及如何避免在主线程上执行昂贵的数据库查询。
最后,Session 还讨论了 Core Data 与 CloudKit 的同步问题——这是 iOS 13 引入但仍然有很多细节需要注意的功能。
值得深挖的点
Faulting 是 Core Data 性能的关键。 当你从数据库中获取一个 NSManagedObject 时,Core Data 并不会立即加载它的所有属性——而是返回一个 Fault(延迟加载的代理对象)。只有在你真正访问某个属性时才会触发数据库查询。理解 Faulting 机制是避免 N+1 查询问题的关键。
perform() vs performAndWait() 的选择。 在后台上下文中操作数据时,perform() 是异步的不会阻塞当前线程,而 performAndWait() 是同步的。Session 明确建议:在主线程的上下文中使用 performAndWait()(因为 UI 需要立即获取数据),在后台上下文中优先使用 perform()。
代码片段
import CoreData
// 正确的并发操作模式
let backgroundContext = persistentContainer.newBackgroundContext()
// 在后台上下文中执行数据库操作
backgroundContext.perform {
let request: NSFetchRequest<Item> = Item.fetchRequest()
request.predicate = NSPredicate(format: "updatedAt > %@", lastSyncDate as NSDate)
do {
let items = try backgroundContext.fetch(request)
// 在后台上下文中处理数据
for item in items {
item.processed = true
}
try backgroundContext.save()
} catch {
print("后台操作失败: \(error)")
}
}
// 批量更新操作(避免加载所有对象到内存)
let batchUpdate = NSBatchUpdateRequest(entity: Item.entity())
batchUpdate.propertiesToUpdate = ["processed": true]
batchUpdate.predicate = NSPredicate(format: "updatedAt < %@", cutoffDate as NSDate)
batchUpdate.resultType = .updatedObjectIDsResultType
do {
let result = try backgroundContext.execute(batchUpdate) as? NSBatchUpdateResult
if let objectIDs = result?.result as? [NSManagedObjectID] {
// 刷新主上下文中的对应对象
let mainContext = persistentContainer.viewContext
mainContext.performAndWait {
objectIDs.forEach { objectID in
guard let object = try? mainContext.existingObject(with: objectID) else { return }
mainContext.refresh(object, mergeChanges: true)
}
}
}
} catch {
print("批量更新失败: \(error)")
}
// 轻量级迁移配置
let container = NSPersistentContainer(name: "MyApp")
let description = container.persistentStoreDescriptions.first
description?.setOption(
true as NSNumber, // 自动迁移
forKey: NSMigratePersistentStoresAutomaticallyOption
)
description?.setOption(
true as NSNumber, // 自动推断映射
forKey: NSInferMappingModelAutomaticallyOption
)
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Core Data 初始化失败: \(error)")
}
}
最佳实践
- 永远不要跨线程传递 NSManagedObject。 正确做法是传递 ObjectID,在目标线程的上下文中通过
existingObject(with:)重新获取。 - 用 NSBatchUpdateRequest 和 NSBatchDeleteRequest 做批量操作。 不要把成千上万条记录加载到内存中再逐个修改,批量操作直接在 SQLite 层面执行。
- 控制 Fault 触发的频率。 如果你知道即将访问某个对象的所有属性,可以在 Fetch Request 中设置
returnsObjectsAsFaults = false,一次性加载全部数据。 - 给 Core Data 模型版本做好版本管理。 每次修改 Schema 都创建新的模型版本,即使你使用轻量级迁移也要保留所有历史版本。
还有什么值得关注
- Core Data with CloudKit 的同步机制在 2020 年更加稳定,但仍有限制(如不支持自定义迁移)
- NSPersistentCloudKitContainer 支持在多个 CloudKit 环境之间切换
- Session 中提到的调试技巧:启动参数
-com.apple.CoreData.SQLDebug 1可以打印所有 SQL 语句 - 新增的 NSPersistentStoreDescription 可以给不同的 Store 配置不同的参数