CoreData是Apple官方为iOS提供的一个数据持久化方案,其本质是一个通过封装底层数据操作,让程序员以面向对象的方式存储和管理数据的ORM框架(Object-Relational Mapping:对象-关系映射,简称ORM)。虽然底层支持SQLite、二进制数据、xml等多种文件存储,但是主要还是用来操作SQLite数据库。
程序员不需要学习或者使用SQL语句,只需要使用CoreData框架提供的对象和接口以及图形化工具,即可完成SQLite数据库的创建、表关系、增删改查等一系列操作,在一定程度上降低了程序员的学习成本并增加了代码的统一性和可阅读性。
框架结构
CoreData作为一个ORM框架,需要解决实体对象和数据库数据的映射关系,将OC对象转化为数据,保存到SQLite文件中,同时能将保存在数据库中的数据还原成OC对象。概要结构如下:

CoreData主要包含以下几个类:
- NSManagedObjectModel:托管对象模型,映射实体类和数据库数据的关系,本质是一个XML文件,后面简称MOM
- NSManagedObject:托管对象,对应数据库数据的实体,后面简称MO
- NSManagedObjectContext:托管对象上下文,管理托管对象,后面简称MOC
- NSPersistentStoreCoordinator:持久化存储调度器,用来处理磁盘持久化数据和实体类对象的相互转化,后面简称PSC
- NSPersistentStore:持久化存储器,负责磁盘持久化数据存取,后面简称PS
CoreData的总体框架如下图:
在上层通过MOC操作对应的托管对象,然后MOC会将操作传递给PSC,PSC通过托管对象模型中的映射关系,再将托管对象的操作转化为对底层数据的操作,进行数据存取操作。
CoreData使用流程
1、创建和配置模型文件
a. 新建项目是勾选Use CoreData,xcode会自动创建『XXX.xcdatamodeld』的模型文件;

b. 如果新建项目时未勾选,可以通过新建文件手动创建;

c. 通过图形化工具即可配置实体类和数据库表的映射,实体类之间的关系,以及简单的查询策略;

d. 配置好模型文件后,xcode8版本之前需要手动生成实体类,选中entity -> 菜单栏Editor -> Create NSManagedObject SubClass即可手动生成托管实体类。xcode8版本以后会自动生成实体类,默认在项目中不会显示。如果想要查看实体类结构,可以在模型文件中配置『Codegen』为『Manual/None』,然后再手动生成,否则会存在两份实体类,编译报错;
e. 对应每个实体类会使用catogory的方式生成如图4个文件,继承自NSManagedObject,托管实体类的属性用@dynamic修饰,CoreData框架会在运行时动态生成存取方法;

f. 这样就完成了数据模型文件创建和简单使用,在代码中初始化即可使用。
1
2
3
4
5
6
7
8
9
10
11
12
13 - (NSManagedObjectModel *)managedObjectModel{
if (nil == _managedObjectModel) {
/**
*从包的根目录获取到模型文件的url,然后通过模型文件初始化managedObjectModel
*模型文件的后缀名和在工程中看到的不一样,工程中为@“xcdatamodeld”,真实为@“momd”
*模型文件本质上是一个苹果自定义的xml文件
*/
NSURL *url = [[NSBundle mainBundle] URLForResource:@"LearnCoreData"
withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:url];
}
return _managedObjectModel;
}
2、使用数据模型MOM初始化持久化存储调度器PSC,然后使用持久化存储类型和路径配置持久存储器PS,最后将PS添加到PSC即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (NSPersistentStoreCoordinator *)psCoordinator{
if (nil == _psCoordinator) {
//1.使用数据模型初始化
_psCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
//2.配置底层文件名和保存路径
NSString *homePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSURL *pathURL = [NSURL fileURLWithPath:[homePath stringByAppendingPathComponent:@"learnCoreData.sql"]];
//3.配置持久化数据存储类型和路径
[_psCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:pathURL
options:nil
error:nil];
}
return _psCoordinator;
}
3、初始化托管对象上下文MOC,并配置队列的类型,队列类型是一个枚举值,跟CoreData多线程相关(后文会详细介绍),有三种类型:
* NSMainQueueConcurrencyType:主并发队列,UI相关操作建议使用该队列
* NSPrivateQueueConcurrencyType:私有并发队列,会在子线程执行操作,耗时操作建议使用
* NSMainQueueConcurrencyType:已废弃
1
2
3
4
5
6
7
8
- (NSManagedObjectContext *)managedOC{
if (nil == _managedOC) {
//初始化ManagedObjectContext并选择类型
_managedOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedOC.persistentStoreCoordinator = self.psCoordinator;
}
return _managedOC;
}
4、将MOC和PSC关联,即完成了CoreData的初始化和配置工作。
基本使用
输出SQL语句日志
使用CoreData操作数据库,默认是没有底层操作数据库的相关SQL语句的,可以通过配置project -> schema -> edit schema - run开启,如下图:

数据库插入操作
- 使用NSEntityDescription类工厂方法传入要插入的托管对象类和指定托管对象所在的上下文获取到托管对象MO
- 对MO进行赋值
- 调用当前MOC的save方法就可以完成数据库的插入操作
1 | /** |
数据库删除操作
- 直接调用托管对象上下文MOC删除管理的托管对象即可完成数据库记录的删除操作
- 正常情况下,当前是没有要删除的托管对象的,因此需要先调用查询找到要删除的MO,然后调用删除
1 | /** |
数据库更新操作
使用CoreData进行数据库更新操作和删除很类似:
- 首先通过查询获取到要更新的托管对象
- 直接对托管对象进行处理
- 将托管对象的更新操作保存到数据库
1 | /** |
数据库查询操作
CoreData将查询操作进行了很好的封装,不需要使用SQL语句,使用NSFetchRequest(查询请求)和NSPredicate(谓词)来完成查询:
- 通过NSFetchRequest指定要查询的类
- 通过NSPredicate指定查询条件
- 调用MOC执行查询请求即可完成查询
- NSFetchRequest提供了参数resultType,是一个枚举类型,可以通过这个参数,设置执行fetch操作后返回的数据类型:
- NSManagedObjectResultType: 返回值是MO的子类,也就是托管对象,这是默认选项
- NSManagedObjectIDResultType: 返回NSManagedObjectID类型的对象,也就是NSManagedObject的ID,对内存占用比较小。MOC可以通过NSManagedObjectID对象获取对应的托管对象
- NSDictionaryResultType: 返回字典类型对象
- NSCountResultType: 返回请求结果的count值,不会加载托管对象到内存
1 | /** |
使用进阶
数据库复杂查询
上一节在初始化CoreData堆栈后,使用MO和MOC,配合NSFetchRequest和NSPredicate即可完成数据库的基本增删改查操作,这一节介绍基于CoreData的复杂查询,而查询的关键在于熟悉和使用NSPredicate。
NSPredicate
官方的解释:
The NSPredicate class is used to define logical conditions used to constrain a search either for a fetch or for in-memory filtering.
NSPredicate类是用来定义逻辑条件约束的获取或内存中的过滤搜索,支持的基本运算如下:
语法 | 作用 |
---|---|
=或== | 判断是否相等 |
<=,=< | 左边小于等于右边 |
>=,=> | 左边大于等于右边 |
>或< | 大于或小于 |
!=、<> | 不相等 |
BETWEEN | 在区间内 |
BEGINSWITH | 字符串以指定开头 |
ENDSWITH | 字符串以指定结尾 |
CONTAINS | 包含指定字符串 |
LIKE | 是否匹配指定模板,用来模糊匹配,例如”name LIKE ‘ab’”或”name LIKE ‘?ab*’” |
MATCHES | 检查某个字符串是否匹配指定的正则表达式 |
ANY、SOME | 集合中任意一个元素满足条件,就返回YES |
ALL | 集合中所有元素都满足条件,返回YES |
NONE | 集合中是否没有元素 |
IN | 等价于SQL语句中的IN运算符 |
SELF | 代表正在被判断的对象自身 |
模糊和多条件查询
1 | /** |
排序和分页
1 | /** |
批量操作
在CoreData中操作大量数据时,如果将大量MO加载到内存再进行数据更新,会占用大量内存,在移动设备有限的内存上明显会造成瓶颈,因此在iOS8 增加了”Batch Updates”,又在 iOS9 增加了”Batch Deletions”,其根本思路是绕开使用MOC将数据全部加载到内存而直接进行底层数据库操作。
批量更新
- 使用批量更新NSBatchUpdateResult,指定要批量更新的MO,即数据库表
- 初始化查询条件
- 通过配置propertiesToUpdate批量修改的属性和和要修改的值,为一个dictionary
- 设置批量更新放回的结果集类型,设置后返回结果会存到NSBatchUpdateResult的result属性中
- NSStatusOnlyResultType :默认值,只返回批量更新的结果 YES:成功 / NO : 失败
- NSUpdatedObjectIDsResultType :返回批量更新后的MO的id数组
- NSUpdatedObjectsCountResultType :返回批量更新后的数量
- 调用MOC执行批量更新操作,并获取结果。由于批量更新会绕过MOC直接进行数据库操作,因此需要重新执行查询或手动同步数据变化到MOC
1 | /** |
批量删除
- 初始化批量删除request,系统提供两种初始化方法
- initWithFetchRequest: 通过fetchRequest设置要删除的数据,需要先初始化fetchRequest指定要删除的数据
- initWithObjectIDs: 使用要删除的托管对象的id来指定要删除的数据,需要先查询出删除对象的id数组
- 和批量更新一样,可以指定批量删除的操作结果
- NSStatusOnlyResultType :默认值,只返回批量删除的结果 YES:成功 / NO : 失败
- NSUpdatedObjectIDsResultType :返回批量删除更新后的MO的id数组,用来同步变化到MOC
- NSUpdatedObjectsCountResultType :返回批量删除后的数量
- 根据操作结果进行相关操作
1 | /** |
批量读取
平常使用CoreData进行查询操作时,使用NSFetchRequest,并调用MOC的executeFetchRequest:error:方法,会更新MOC管理的MO,然后直接执行并返回结果集。这个过程是同步的,会阻塞当前的MOC。
针对该问题,iOS增加了Asynchronous Fetching查询方法,执行后不会马上返回执行后的结果,并且不会阻塞当前的MOC,测试发现会先执行其它的数据库操作,操作完后执行异步查询。最后执行完后会执行回调block,暂时未发现这种方式的引用场景。
1 | /** |
高级特性
NSFetchedResultsController
NSFetchedResultsController(后面简称FRC)从命名看很像是一个Controller,其实不然。FRC是CoreData提供给UITableView或UICollectionView跟底层持久化数据交互的控制器(胶水代码),类似数据源管理的角色。结构如下:

NSFetchedResultsController绑定一个MOC后,当该MOC进行save操作将数据存储到底层时,如果判断是IUD(I:Insert、U:Update、D:Delete)操作,则会通知绑定的FRC触发相应的回调方法。
NSFetchedResultsController基本使用
- 使用fetchRequest指定要查询的数据表
- 使用NSSortDescriptor指定查询后的排序条件
- 使用fetchRequest初始化FRC并绑定MOC
- 在合适的位置调用FRC执行查询获取数据
- 设置FRC的代理,在代理方法中做处理
1 | //初始化FRC |
结合UITableView使用
FRC本身就是针对UITableView和UICollectionView封装的,接下来结合UITableView描述如何使用。当MOC进行数据库操作时,如果是IDU操作时,触发FRC相应回调,执行代理方法并传递相应变化类型。类型如下:
- NSFetchedResultsChangeInsert:绑定的MOC的托管对象出现插入变化
- NSFetchedResultsChangeDelete:绑定的MOC的托管对象出现删除变化
- NSFetchedResultsChangeMove:绑定的MOC的托管对象出现移动变化
- NSFetchedResultsChangeUpdate:绑定的MOC的托管对象出现更新变化
结合UITableView和FRC实现增删改移的双向同步操作:
1、在Table view data source的代理方法中直接调用FRC,返回section数量、标题以及row数量;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#pragma mark - Table view data source
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
//FRC中封装好了section数组和section标题
return self.fetchedResultsController.sections[section].indexTitle;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
//FRC中封装好了section数组,返回数组长度
return self.fetchedResultsController.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//FRC中封装好了section数组,直接获取相应section的row数量
return [self.fetchedResultsController.sections objectAtIndex:section].numberOfObjects;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//FRC中封装好了直接通过indexPath获取相应的对象
Student *student = [self.fetchedResultsController objectAtIndexPath:indexPath];
static NSString *identifier = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (nil == cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:identifier];
}
// Configure the cell...
cell.textLabel.text = student.studentName;
cell.detailTextLabel.text = student.studentSex == 0 ? @"男" : @"女";
return cell;
}
2、通过UITableView的编辑模式进行cell的删除、拖动操作时,使用FRC获取到对应的托管对象,然后进行相应的delete或move操作,并在FRC的回调方法中进行相应处理;
1 | // Override to support conditional editing of the table view. |
3、使用FRC绑定的MOC进行数据库插入和更新操作,同样会触发FRC,执行代理方法;
1 | /** |
4、在FRC的代理方法中执行UITableView的cell变化操作,相应代码和详细注释如下:
1 | #pragma mark -NSFetchedResultsControllerDelegate |
版本迁移
为什么要进行数据库版本迁移?
应用在更新迭代的过程中,最初设计的数据库无法满足业务需求,需要修改数据库结构,但是客户端的持久化数据还保存在用户的设备磁盘中,因此需要跟随数据库版本的升级将低版本数据库结构存储的数据迁移到升级后的数据库中。这种情况可以通过良好的数据库结构设计减少迁移次数,但无法避免。其本质是新建新版本数据结构的数据库,通过每种映射或规则将老版本数据结构的数据添加到新版本的数据库中,然后删除老版本数据库,完成数据的迁移。CoreData提供了三种数据迁移方式:1.自带轻量级数据迁移;2.创建Mapping文件进行迁移;3.代码实现数据迁移。本文只进行简单介绍,后续会有专门详细说明的文章。

自带轻量级数据迁移
适用场景:当新版本数据库只是在老版本基础上进行数据表新增、原表新增属性操作时。
创建Mapping文件进行迁移
适用场景:复杂的数据迁移,例如旧表数据迁移到新表并对数据做处理、表删除、实体属性删除等,通过mapping文件建立源数据表数据到目标数据表的映射。
代码实现数据迁移
适用场景:监控复杂数据迁移的过程,进度等,例如Mapping文件迁移需要加载老版本和新版本的数据库的数据到内存,存在内存达到峰值被系统kill的风险,可以使用代码迁移将任务拆分。
多线程处理
CoreData中MOC(托管对象上下文)本身不是线程安全的,因此不能跨线程使用同一个MOC,而PSC(持久化存储解析器)是线程安全的,多个托管对象上下文可以使用同一个持久化存储解析器。因此有两种多线程方案,本质都是将耗时操作放到子线程,UI操作放在主线程(UI优先级最高),保证主线程的同步和流畅。
MOC初始化可以指定并发队列类型:
- NSConfinementConcurrencyType : 该类型已经被启用。同NSMainQueueConcurrencyType;
- NSPrivateQueueConcurrencyType : 私有并发队列类型,操作将在子线程中执行;
- NSMainQueueConcurrencyType : 主并发队列类型,操作在主线中执行,如果涉及到UI相关的操作,应该使用该种类型。
通知模式多线程
使用MOC通知:当MOC进行save到本地操作时会发送NSManagedObjectContextDidSaveNotification通知。

- 子线程初始化自己的托管对象上下文
privateContext
; - 主线程初始化自己的托管对象上下文
mainContext
并监听parivateContext
的这个通知; - 子线程执行完耗时操作后进行本地save;
mainContext
在主线程接受到通知,进行merge操作并更新UI;- 这样将耗时操作和disk io都放到了子线程,同时也保证了主线程托管对象上下文的同步。
1 | /** |
父子Context模式多线程
MOC提供设置父子关系,子MOC进行save操作时会将变化push到父MOC。

- 创建多个context,两个子线程
privateContext1
和privateContext2
,主线程mainContext
,继承关系privateContext1
->mainContext
->privateContext2
,建立一个三层关系,其中只有最上层privateContext2持有PSC; - 在
privateContext1
中进行耗时数据库操作,执行完成后,使用save方法push到mainContext
,则mainContext
可以同步到托管对象的而变动,然后使用save方法push到privateContext2
,在privateContext2
中进行save到本地,进行disk io; - 这样将数据库耗时操作和disk IO都放到子线程,同时代码简洁,结构更好,推荐使用这种方式。
1 | /** |
优缺点和解决方案
优点
- 将数据库操作和SQL语句转化为OC对象的操作,将关系型数据库操作转换为面向对象操作,程序员不需要关心底层的SQL语句,提升了代码的易读性和理解性,降低难度
- 提供图形化工具操作数据库,减少代码量,使用简单直观
- 查看数据库表结构和关系方便直观,降低数据库熟悉成本
- 建表、表属性、表之间关系操作方便简洁
- 使用图形化工具进行数据库迁移,简单直观
- 封装了NSFetchedResultsController,在UITableView和UICollectionView使用数据库数据作为数据源时,极大简化了数据变化和cell之间的同步
- 通过托管对象进行数据库操作非常简单
缺点
- 不提供主键,需要程序员自己维护,为了维护数据库当做主键属性的唯一性,存储之前需要先做数据库查询
- 每个MOC维护自己的MO在内存中,存在数据在内存中的多个副本数据重复的问题,浪费内存空间;不同MOC需要程序员自己处理数据同步问题;
- 需要程序员自己实现多线程数据同步问题
- MO非常容易出现跨线程使用引发野指针等问题
- 只能通过CoreData框架提供的API进行数据库操作,不提供类似SQL语句这种操作,限制了灵活性,例如:多表联合查询使用SQL语句很方便,使用CoreData很复杂
- 没有加密功能,需要程序员自己封装
解决方案
综上所述使用CoreData应该充分利用其提供的图形化工具和对象,然后解决内存、多线程问题和跨线程使用MOC、MO的问题。
内存、多线程解决思路
随着移动设备的硬件升级,单个或少数几个MOC的数据库数据的内存占用是能够接受的,主要需要防止滥用MOC造成数据的大量副本。
- 可以参考
父子Context模式多线程
中整个应用采用三层MOC架构,数据库存储到文件操作放到顶级子线程的MOC中,UI相关操作放到主线程MOC中,其它耗时操作放到第三层子线程的MOC中,尽量保证只使用这三个MOC。如果确实需要拆分耗时操作到不同的子线程中,用完及时释放子线程和MOC。 - 进行查询操作时,如果不需要完整对象,可以设置查询的返回结果为ManagedObjectID
跨层使用MO解决思路
CoreData堆栈中MOC管理的MO对象,对应着数据库持久化数据,上层如果直接用来作为业务层的数据流,在MVC中流动或者在Controller之间传递,根据单一原则,该对象已经不是独立的对应数据库了,会造成很强的依赖性和耦合行;并且由于MO的生命周期由MOC管理,不由Controller控制,当数据库发生变化时,会被修改或删除,跨线程时则更加复杂,因此会出现野指针等问题。
使用SQLite或FMDB不会出现该问题,因为SQLite或FMDB交付给上层的是通用类型的数据,程序员自己转换为OC对象使用,生命周期可控。
解决该问题的思路是MO本身应该只用来表征数据库数据,上层需要使用时应该将MO转换为自己可控的OC对象,不要直接使用MO。因此可以针对MO建立对应的OC对象,并将数据库操作封装一层到对应的OC对象中,并将MO转OC对象统一抽象出来,最后相当于对CoreDate做了一层封装,这样跨线程使用OC对象也不会有问题。使用协议的方式让OC对象实现基本的增删改查操作,详细的设计和实现可以参考这篇文章。

总结
CoreData是Apple官方提供的一个持久化方案,而不仅仅是一个ORM框架,为了实现现在的功能进行了大量封装和设计,包含大量类。与SQLite或者FMDB相比,作者认为图形化工具是最大的亮点,但使用过程中需要注意上面描述的几点。到底使用哪种还是要结合业务场景来做选择,但是无论是使用哪种方案,都用该在其基础上封装一层再供上层使用,并且建立良好的数据库设计文档机制,方便查阅和减少熟悉数据库成本。
以上就是作者对CoreData的学习和理解,如有需要改进的地方欢迎大家指正。
全文Demo在这里