Core Data: Sundries and maxims
Swift & UI 进阶 26m

Core Data:杂项与最佳实践

Core Data: Sundries and maxims

2020年6月24日

在 Apple 官方观看视频

一句话判断

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 配置不同的参数
WWDC 2020