Core Data入门:iOS数据持久化方案,一个裸辞老咸鱼的踩坑实录
去年10月,我裸辞了。
不是因为受不了996(虽然也挺烦的),也不是和PM干架(虽然他确实想让我在双11前用SwiftUI重写整个App的数据库层),纯粹就是……想喘口气。Gap了半年,刷LeetCode、看操作系统源码、折腾Vim插件,顺便把《深入理解计算机系统》翻烂了。但现实很快打脸——简历投出去石沉大海,HR问:“最近项目经验有点空档啊?” 我只能苦笑:总不能说我在研究x86指令流水线吧?
于是,为了重返职场,我决定接个Side Project练手,顺便给简历加点“近期产出”。选了个记账类App,轻量、实用、还能展示数据持久化能力——毕竟面试官最爱问“你怎么存数据?”
一开始我想直接上SQLite + FMDB,毕竟底层控嘛,连SQL语句都手写得飞起。结果朋友一句灵魂拷问把我拉回现实:“你打算自己处理多线程并发?关系映射?迁移方案?别了吧,Core Data它不香吗?”
行吧,那就Core Data。但没想到,这个Apple亲儿子,坑比后端同事写的API还深。
第一坑:以为是ORM,结果是个状态机
我天真的以为Core Data就是个简单的对象-关系映射工具,像Java里的Hibernate。结果一上手就懵了:NSManagedObjectContext、NSPersistentStoreCoordinator、NSManagedObjectModel……这仨兄弟到底谁管谁?
查文档发现,Core Data根本不是数据库,而是一个对象图管理框架。它的核心是“上下文”(Context),所有操作都在Context里进行,最后才提交到持久化存储(比如SQLite)。这意味着:
- 你在主线程改了个对象,如果不
save(),其他线程根本看不到 - 多个Context之间数据同步要手动merge
- 后台Context保存后,主线程不会自动刷新UI
上周五晚上我正调试一个“新增账单不显示”的Bug,死活找不到原因。最后发现:我在后台队列里创建并保存了Expense对象,但主线程的Context没收到通知!当时真的想砸MacBook——这设计反人类吗?不,这是Apple式优雅(强行说服自己)。
解决方案?用NSManagedObjectContextDidSaveNotification监听保存事件,然后在主线程调用mergeChanges(from:)。代码长这样:
// 在AppDelegate或DataManager初始化时注册
NotificationCenter.default.addObserver(
self,
selector: #selector(mainContextUpdated(_:)),
name: .NSManagedObjectContextDidSave,
object: backgroundContext
)
@objc private func mainContextUpdated(_ notification: Notification) {
DispatchQueue.main.async {
self.mainContext.mergeChanges(fromContextDidSave: notification)
// 别忘了刷新UI!
self.tableView.reloadData()
}
}
记住:Core Data默认不跨线程同步,这是无数人踩的第一个大坑。
第二坑:Migration?不,是噩梦
产品上线两周后,PM跑来说:“用户反馈想给账单加个‘标签’字段!” 行,小需求,加个tags: String?属性完事。
结果一运行,App闪退。控制台无情输出:
The model used to open the store is incompatible with the one used to create the store.
哦,对了,Core Data的模型一旦生成,就不能随便改。改了就得做数据迁移(Migration)。
我第一反应是“轻量级迁移”(Lightweight Migration),Apple文档说只要勾两个选项就行:
NSInferMappingModelAutomaticallyOption = trueNSSQLitePragmasOption = ["journal_mode": "WAL"]
于是我在persistentContainer初始化时加了这些:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "ExpenseModel")
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Core Data load failed: \(error)")
}
}
return container
}()
本地测试通过,开心地提交TestFlight。结果第二天,测试妹子发来崩溃日志——部分老用户升级后直接白屏!
排查发现:轻量级迁移只支持简单变更,比如新增非空字段、重命名属性(需设置renamingIdentifier)。但我这次加的是可选String,理论上没问题啊?
后来才知道:如果用户的设备上App版本太旧(比如还在iOS 13),某些迁移场景会失败。更惨的是,一旦迁移失败,Core Data会直接删掉旧数据库(?!),导致用户数据全丢。
血的教训:永远不要依赖自动迁移处理生产数据。现在我的做法是:
- 每次模型变更,手动写
Mapping Model - 提供数据导出/导入功能(JSON备份)
- 在App启动时检测版本,必要时引导用户备份
第三坑:性能?先学会别卡主线程
为了炫技,我在列表页直接用NSFetchedResultsController绑定Core Data到UITableView。滑动流畅,自动更新,Apple官方推荐,完美!
直到我把账单条目加到5000条。
滚动开始掉帧,甚至卡死。Instrument一看,主线程被faulting(懒加载触发)占满。原来Core Data默认是“按需加载”,每次访问未加载的属性都会去磁盘读——这在列表滚动时简直是灾难。
解决方法有俩:
- 预取(Prefetching):告诉Core Data提前加载哪些关系字段
- 后台加载 + 分页
我现在用的是分页方案,配合fetchBatchSize:
let request: NSFetchRequest<Expense> = Expense.fetchRequest()
request.fetchBatchSize = 20 // 每次只加载20条到内存
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
同时,把复杂查询(比如按月汇总)放到后台Context执行,结果回调主线程更新UI。虽然麻烦点,但至少不会被用户骂“这App怎么这么卡”。
和后端比?Core Data其实挺“温柔”
说到这儿,可能有人觉得Core Data太复杂,不如直接调后端API存数据。但别忘了:移动端的核心优势就是离线体验。
我们之前有个项目,后端同事坚持“所有数据必须实时同步”,结果地铁里打开App一片空白。用户差评如潮。后来我硬塞进Core Data做本地缓存,网络恢复后再同步——留存率直接涨了15%。
而且,Apple生态下,Core Data和CloudKit集成简直无缝。一行代码开启iCloud同步:
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption("iCloud.Example.Expense" as NSString, forKey: NSPersistentStoreUbiquitousContentNameKey)
虽然审核时被苹果问了三次“你的iCloud数据结构是什么”,但最终顺利上架。相比之下,自己搞WebSocket+本地缓存+冲突解决?那工作量够我再裸辞一次。
给准备跳槽的同行一点建议
如果你也在刷题、看源码、Gap期焦虑,不妨像我一样做个完整的小项目。Core Data看似过时,但在Apple生态里仍是最合规、最省审的数据方案。面试时聊清楚它的线程模型、迁移策略、性能优化,绝对比背八股文加分。
顺便,我的Side Project已经上架App Store,名字叫“极简账本”(别搜了,下载量不到100 😅)。虽然没赚到钱,但简历上终于能写“独立开发并发布iOS应用,采用Core Data实现本地持久化与iCloud同步”。
技术分享的意义,不就是把踩过的坑变成别人的垫脚石吗?
附:Core Data vs 其他方案速查表
| 方案 | 适合场景 | 学习成本 | 线程安全 | 审核友好度 |
|---|---|---|---|---|
| Core Data | Apple生态、离线优先 | 中高 | 需手动管理 | ⭐⭐⭐⭐⭐ |
| SQLite + FMDB | 底层控制、跨平台 | 中 | 需加锁 | ⭐⭐⭐ |
| UserDefaults | 小量配置 | 低 | 自带同步 | ⭐⭐⭐⭐ |
| Realm | 高性能、复杂查询 | 中 | 较好 | ⭐⭐(曾被拒) |
| 直接调后端 | 实时性要求高 | 低 | 依赖网络 | ⭐⭐⭐ |
记住:没有银弹,只有权衡。而作为前大厂员工,我现在最大的感悟是——能跑就行,别过度设计。毕竟,PM明天可能又要改需求了。

评论 0