【Core Data】Apple 官方数据持久化框架——对象图管理与 SQLite 存储的终极方案
iOS三方库精读 · 第 20 期
一、一句话介绍
Core Data 是 Apple 提供的对象图管理与数据持久化框架,它让 iOS/macOS 应用中的结构化数据存储、查询、关系维护和版本迁移变得声明式且高度集成。
| 属性 | 信息 |
|---|---|
| 发布时间 | macOS Tiger (10.4) / iOS 3.0 |
| 维护者 | Apple(系统框架) |
| License | Apple Platform SDK |
| 支持平台 | iOS / macOS / tvOS / watchOS |
| 语言 | Objective-C + Swift 桥接 |
二、为什么选择它
没有 Core Data 时的痛点
| 痛点 | 原始做法 | Core Data 解决方案 |
|---|---|---|
| 手写 SQL 维护成本高 | 直接操作 SQLite C API | NSManagedObject 自动映射 |
| 对象关系手动维护 | Dictionary / 自建 ORM | Relationship + Inverse 自动管理 |
| 数据模型升级困难 | ALTER TABLE 手写 SQL | Lightweight Migration 自动迁移 |
| 并发访问不安全 | 加锁 / GCD 串行队列 | 每个 Context 独立队列 + perform {} |
| UI 数据不同步 | NotificationCenter 手动通知 | NSFetchedResultsController 实时驱动 |
| iCloud 同步无方案 | 自建同步服务 | NSPersistentCloudKitContainer 开箱即用 |
Core Data 的核心优势:
- Apple 一等公民:Xcode 可视化数据模型编辑器、SwiftUI @FetchRequest 原生集成
- 对象图管理:自动维护实体间关系(一对一、一对多、多对多)和级联删除
- 延迟加载(Faulting):按需加载数据,大幅减少内存占用
- 数据验证:属性级和实体级验证规则内置
- Undo/Redo:与 UndoManager 无缝集成
- CloudKit 同步:NSPersistentCloudKitContainer 一行代码启用 iCloud 同步
三、核心功能速览
基础层(新手必读)
环境集成
Core Data 是系统框架,无需 SPM/CocoaPods 集成,直接 import CoreData 即可。
Xcode 创建项目时勾选 "Use Core Data" 会自动生成 .xcdatamodeld 文件和 PersistenceController。
Core Data Stack 四件套
// 1. NSManagedObjectModel —— 数据模型(.xcdatamodeld 编译产物)
// 2. NSPersistentStoreCoordinator —— 存储协调器
// 3. NSPersistentContainer —— 容器(封装了上述两者)
// 4. NSManagedObjectContext —— 托管对象上下文(CRUD 都在这里)
let container = NSPersistentContainer(name: "MyModel")
container.loadPersistentStores { desc, error in
if let error { fatalError("Core Data 加载失败: \(error)") }
}
let context = container.viewContext
基础 CRUD
// Create
let item = Item(context: context)
item.title = "学习 Core Data"
item.createdAt = Date()
try context.save()
// Read
let request = NSFetchRequest<Item>(entityName: "Item")
request.predicate = NSPredicate(format: "title CONTAINS[cd] %@", "Core Data")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Item.createdAt, ascending: false)]
let items = try context.fetch(request)
// Update
item.title = "已完成学习"
try context.save()
// Delete
context.delete(item)
try context.save()
进阶层(最佳实践)
NSFetchedResultsController(UIKit 必备)
let frc = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: "category",
cacheName: "itemsCache"
)
frc.delegate = self
try frc.performFetch()
// UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
frc.sections?.count ?? 0
}
@FetchRequest(SwiftUI 原生集成)
struct ItemListView: View {
@FetchRequest(
sortDescriptors: [SortDescriptor(\.createdAt, order: .reverse)],
predicate: NSPredicate(format: "isDone == NO"),
animation: .default
)
private var items: FetchedResults<Item>
var body: some View {
List(items) { item in
Text(item.title ?? "")
}
}
}
后台上下文
// 在后台线程批量导入数据
container.performBackgroundTask { bgContext in
for data in largeDataSet {
let item = Item(context: bgContext)
item.title = data.title
}
try? bgContext.save()
// viewContext 自动合并(需设置 automaticallyMergesChangesFromParent = true)
}
NSPredicate 常用表达式
// 字符串匹配(不区分大小写和变音符号)
NSPredicate(format: "title CONTAINS[cd] %@", searchText)
// 范围查询
NSPredicate(format: "createdAt >= %@ AND createdAt <= %@", startDate, endDate)
// 关系查询
NSPredicate(format: "ANY tags.name == %@", "重要")
// 聚合
NSPredicate(format: "SUBQUERY(tasks, $t, $t.isDone == YES).@count > 0")
深入层(源码视角)
核心模块职责
| 类 | 职责 |
|---|---|
NSManagedObjectModel | 数据模型定义,编译自 .xcdatamodeld |
NSPersistentStoreCoordinator | 管理底层存储(SQLite/XML/Binary/In-Memory) |
NSPersistentContainer | 便利容器,封装 Model + Coordinator + Context |
NSManagedObjectContext | 对象图的内存工作区,所有 CRUD 操作的入口 |
NSManagedObject | 托管对象基类,属性通过 KVC 动态派发 |
NSFetchRequest | 类型安全的查询描述 |
NSFetchedResultsController | 查询结果监听器,驱动 UITableView/UICollectionView |
Faulting 机制
┌─────────────────┐
│ NSManagedObject │
│ (Fault 状态) │
│ 仅含 objectID │
└────────┬────────┘
│ 首次访问属性
▼
┌─────────────────┐
│ NSManagedObject │
│ (Fulfilled 状态)│
│ 所有属性已加载 │
└─────────────────┘
Core Data 默认返回 Fault 对象(仅包含 objectID),只有在首次访问属性时才从存储中加载数据。这大幅减少了内存占用,但也意味着:
- 避免在循环中逐个访问属性(触发 N+1 问题)
- 批量查询时使用
request.returnsObjectsAsFaults = false预加载
写入事务模型
Core Data 的 save 操作本质上是一个事务:Context 追踪所有 insert/update/delete,save 时一次性写入 SQLite。如果 save 失败,整个事务回滚。
四、实战演示
纯代码构建 Core Data Stack(无需 .xcdatamodeld)
// 适合 Demo/测试/动态模型场景
final class CoreDataDemoStack: ObservableObject {
let container: NSPersistentContainer
init() {
let model = NSManagedObjectModel()
let entity = NSEntityDescription()
entity.name = "TodoItem"
entity.managedObjectClassName = "TodoItem"
let titleAttr = NSAttributeDescription()
titleAttr.name = "title"
titleAttr.attributeType = .stringAttributeType
let createdAtAttr = NSAttributeDescription()
createdAtAttr.name = "createdAt"
createdAtAttr.attributeType = .dateAttributeType
let isDoneAttr = NSAttributeDescription()
isDoneAttr.name = "isDone"
isDoneAttr.attributeType = .booleanAttributeType
isDoneAttr.defaultValue = false
entity.properties = [titleAttr, createdAtAttr, isDoneAttr]
model.entities = [entity]
container = NSPersistentContainer(name: "Demo", managedObjectModel: model)
let desc = NSPersistentStoreDescription()
desc.type = NSInMemoryStoreType // 内存存储,适合 Demo
container.persistentStoreDescriptions = [desc]
container.loadPersistentStores { _, error in
if let error { fatalError("加载失败: \(error)") }
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
完整 CRUD + 查询 Demo
let context = stack.container.viewContext
// Create
let item = TodoItem(context: context)
item.title = "阅读 Core Data 文档"
item.createdAt = Date()
try context.save()
// Read with filter
let request = NSFetchRequest<TodoItem>(entityName: "TodoItem")
request.predicate = NSPredicate(format: "isDone == NO")
request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
let pendingItems = try context.fetch(request)
// Aggregate query
let countRequest = NSFetchRequest<TodoItem>(entityName: "TodoItem")
countRequest.predicate = NSPredicate(format: "isDone == YES")
let doneCount = try context.count(for: countRequest)
// Batch delete (iOS 9+)
let batchDelete = NSBatchDeleteRequest(fetchRequest: NSFetchRequest(entityName: "TodoItem"))
batchDelete.resultType = .resultTypeCount
let result = try context.execute(batchDelete) as? NSBatchDeleteResult
print("删除了 \(result?.result ?? 0) 条记录")
五、源码亮点
进阶层:值得借鉴的用法
1. NSPersistentContainer 的便利设计
// 一个 Container 封装了整个 Core Data Stack
// 对比 iOS 10 之前需要手动创建:
// NSManagedObjectModel → NSPersistentStoreCoordinator → NSManagedObjectContext
// NSPersistentContainer 将这些全部内聚
2. perform {} 的线程安全模式
// Context 的每个操作都绑定到特定队列
context.perform {
// 在 context 的队列上执行,线程安全
let item = Item(context: context)
item.title = "安全创建"
try? context.save()
}
context.performAndWait {
// 同步版本,阻塞当前线程直到完成
}
深入层:设计思想解析
1. Unit of Work 模式
Core Data 的 NSManagedObjectContext 是经典的 Unit of Work 实现:
- 追踪所有变更(insert/update/delete)
- save 时一次性提交到持久化存储
- 支持 rollback 回滚所有未保存变更
- 类似 Git 的暂存区:修改 → stage → commit
2. 观察者模式(NSFetchedResultsController)
NSFetchedResultsController 监听 Context 的 NSManagedObjectContextDidSave 通知,自动计算 diff 并通过 delegate 回调插入/删除/移动/更新动画。这是 Apple 版的 "ListDiff + UITableView 自动刷新"。
3. Faulting 的代理模式
NSManagedObject 在 fault 状态下是一个代理对象,仅持有 objectID。当首次访问属性时,Core Data 通过 willAccessValue(forKey:) / didAccessValue(forKey:) 触发数据加载。这是经典的 Virtual Proxy 模式。
六、踩坑记录
问题 1:在错误线程访问 Context 原因:NSManagedObjectContext 绑定到创建它的队列,跨线程访问会导致崩溃或数据损坏 解决:
// ✅ 始终使用 perform {} 包裹
context.perform {
let items = try? context.fetch(request)
}
// ❌ 直接在其他线程操作
DispatchQueue.global().async {
let items = try? context.fetch(request) // 危险!
}
问题 2:NSBatchDeleteRequest 不更新内存对象 原因:批量删除直接操作 SQLite,绕过了 Context 的变更追踪 解决:
let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)
batchDelete.resultType = .resultTypeObjectIDs
let result = try context.execute(batchDelete) as? NSBatchDeleteResult
if let objectIDs = result?.result as? [NSManagedObjectID] {
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSDeletedObjectsKey: objectIDs],
into: [context]
)
}
问题 3:Lightweight Migration 失败 原因:模型变更超出自动迁移范围(如重命名实体、改变关系类型) 解决:
let desc = NSPersistentStoreDescription()
desc.shouldMigrateStoreAutomatically = true
desc.shouldInferMappingModelAutomatically = true
// 复杂迁移需创建 NSMappingModel 手动指定映射规则
问题 4:@FetchRequest 在 sheet/NavigationLink 内崩溃 原因:子视图没有接收到 managedObjectContext 环境值 解决:
.sheet(isPresented: $showDetail) {
DetailView()
.environment(\.managedObjectContext, viewContext) // 必须传递
}
问题 5:合并冲突(Merge Conflict) 原因:多个 Context 同时修改同一对象 解决:
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// 或使用自定义 NSMergePolicy
问题 6:内存暴涨(大批量导入) 原因:Context 追踪了所有 insert 对象,未及时释放 解决:
container.performBackgroundTask { bgContext in
bgContext.undoManager = nil // 禁用 undo,减少内存
for (i, data) in largeDataSet.enumerated() {
let item = Item(context: bgContext)
item.title = data.title
if i % 1000 == 0 {
try? bgContext.save()
bgContext.reset() // 释放内存
}
}
try? bgContext.save()
}
七、延伸思考
Core Data vs Swift Data vs GRDB
| 维度 | Core Data | Swift Data | GRDB |
|---|---|---|---|
| 最低版本 | iOS 3.0 | iOS 17.0 | iOS 13.0 |
| API 风格 | ObjC 风格 | Swift 原生 | SQL + Swift |
| 类型安全 | 需强转 | 完全安全 | Codable |
| 并发处理 | perform {} 手动管理 | @ModelActor 自动 | DatabasePool |
| iCloud 同步 | CloudKit Container | 内置支持 | 无 |
| 学习曲线 | 陡峭 | 平缓 | 中等 |
| 迁移策略 | Lightweight + Mapping | 自动 | DatabaseMigrator |
| SQL 控制 | 无(NSPredicate) | 无 | 完整 SQL |
| 包体积 | 系统框架(0 KB) | 系统框架(0 KB) | ~500 KB |
推荐场景
选 Core Data:
- 需要支持 iOS 16 及以下版本
- 已有 Core Data 存量代码的项目
- 需要 CloudKit 同步且目标低于 iOS 17
- 需要 NSFetchedResultsController 驱动 UIKit 列表
不选 Core Data:
- 新项目且最低支持 iOS 17+(选 Swift Data)
- 需要复杂 SQL 查询和 JOIN(选 GRDB)
- 仅需简单 KV 存储(选 UserDefaults / Keychain)
当前状态
Core Data 不会被 deprecated。Swift Data 底层就是 Core Data,Apple 在 WWDC 2023 明确表示两者将长期共存。对于需要兼容旧版本的项目,Core Data 仍然是最佳选择。
八、参考资源
- Apple Core Data Programming Guide
- Apple Core Data API Reference
- WWDC 2019 - Making Apps with Core Data
- WWDC 2020 - Core Data: Sundries
- WWDC 2021 - Bring Core Data concurrency to Swift and SwiftUI
- WWDC 2022 - Evolve your Core Data schema
- Donny Wals - Practical Core Data (2021)
- NSHipster - Core Data
本期互动
小作业
用纯代码(不使用 .xcdatamodeld 文件)创建一个包含两个实体(User 和 Post,一对多关系)的 Core Data Stack,并实现:创建 User → 为 User 添加 Post → 查询某 User 的所有 Post。在评论区贴出你的 NSManagedObjectModel 构建代码。
思考题
Core Data 的 Faulting 机制在大部分场景下减少了内存占用,但在某些场景下反而会导致性能下降(N+1 问题)。如果让你设计一个"智能预加载"策略,你会怎么判断哪些关系应该预加载、哪些应该保持 Fault?
读者征集
下一期选题投票 / 你在使用 Core Data 时踩过哪些坑?尤其是并发冲突、迁移失败、CloudKit 同步问题,欢迎评论区分享,优质回答会收录进下一期《踩坑记录》。
📅 本系列每周五晚更新 · ✅ 第18期:Kingfisher · ✅ 第19期:GRDB · ➡️ 第20期:Core Data · ○ 第21期:待定