【Core Data】Apple 官方数据持久化框架——对象图管理与 SQLite 存储的终极方案

6 阅读8分钟

【Core Data】Apple 官方数据持久化框架——对象图管理与 SQLite 存储的终极方案

iOS三方库精读 · 第 20 期


一、一句话介绍

Core Data 是 Apple 提供的对象图管理与数据持久化框架,它让 iOS/macOS 应用中的结构化数据存储、查询、关系维护和版本迁移变得声明式且高度集成

属性信息
发布时间macOS Tiger (10.4) / iOS 3.0
维护者Apple(系统框架)
LicenseApple Platform SDK
支持平台iOS / macOS / tvOS / watchOS
语言Objective-C + Swift 桥接

二、为什么选择它

没有 Core Data 时的痛点

痛点原始做法Core Data 解决方案
手写 SQL 维护成本高直接操作 SQLite C APINSManagedObject 自动映射
对象关系手动维护Dictionary / 自建 ORMRelationship + Inverse 自动管理
数据模型升级困难ALTER TABLE 手写 SQLLightweight Migration 自动迁移
并发访问不安全加锁 / GCD 串行队列每个 Context 独立队列 + perform {}
UI 数据不同步NotificationCenter 手动通知NSFetchedResultsController 实时驱动
iCloud 同步无方案自建同步服务NSPersistentCloudKitContainer 开箱即用

Core Data 的核心优势:

  1. Apple 一等公民:Xcode 可视化数据模型编辑器、SwiftUI @FetchRequest 原生集成
  2. 对象图管理:自动维护实体间关系(一对一、一对多、多对多)和级联删除
  3. 延迟加载(Faulting):按需加载数据,大幅减少内存占用
  4. 数据验证:属性级和实体级验证规则内置
  5. Undo/Redo:与 UndoManager 无缝集成
  6. 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 DataSwift DataGRDB
最低版本iOS 3.0iOS 17.0iOS 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 仍然是最佳选择。


八、参考资源


本期互动

小作业

用纯代码(不使用 .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期:待定