数据库:Core Data

CoreData大纲

一.简介

Core Data 是一个对象-关系映射持久化方案,处于应用与持久化存储之间,可以将应用中的对象转化成数据,保存在SQLite文件等持久化存储中,也可以缓存临时数据。在单台设备上 Core Data 支持回滚重做,在多台设备间使用同一 iCloud 账户同步数据时,Core Data 会自动将结构映射到 CloudKit 中。

.xcdatamodeld数据模型中,可以定义数据类型与关系、生成对应的类定义。Core Data 会自动管理数据对象并提供以下功能:

  • 持久化

抽象化映射对象到持久化存储中的细节,让保存数据更加简单,你甚至不用写数据库管理代码。

持久化存储包括四种:SQLite, Binary, XML, In-Memory,其中 XML 在 iOS 中不可用。

从持久化存储中存-取数据

  • 回滚&重做

Core Data 的回滚管理器会跟踪数据变化,支持单独、批量或者全部回滚数据。

摇晃设备回滚数据功能

  • 在后台处理任务

支持在后台处理会堵塞UI线程的任务,例如将JSON解析成对象。稍后你可以将结果缓存或保存起来,节省与服务器的数据交互次数。

更新UI前在后台处理数据

  • 视图同步

tablecollection视图提供数据源,帮助处理视图与数据的同步。

  • 版本与迁移

.xcdatamodeld文件设置版本,支持在model变化后将数据迁移到最新版本。

二.Model-模型

.xcdatamodeld文件用来定义应用中数据对象的结构,包括数据对象的类型、属性、关系。可通过新建文件Data Model手动创建,其默认文件名为Model.xcdatamodeld

.xcdatamodeld文件

三.Entity-实体

.xcdatamodeld=Entity=,Entity 描述的是一个对象,包括它的名字、属性、关系。在.xcdatamodeld编辑器侧边栏中表示为Entity,可配置的字段包括:

实体配置面板

1.Entity Name

model文件中实体的名字。

2.Abstract Entity

若实体只作为父类使用而不会被直接实例化,可勾选此复选框。默认未勾选,即创建具体的实体。

3.Parent Entity

当有多个属性相似的实体时,可在一个父实体中定义通用属性,然后让子实体继承这些属性。

4.Class Name

以实体为基础创建托管对象实例时所指定的类名,默认与实体名相同。

5.Codegen

.xcdatamodeld编辑界面定义好实体后,还需要生成对应的托管对象类和属性文件,以便在实际开发中创建和使用实体的实例。这些文件可以手动生成,也可以让 Core Data 帮我们自动生成。我们需要做的是为Codegen选项指定不同的值。以下是选项值与具体场景的对应关系:

选项
场景
Class Definition 由Core Data自动生成托管对象类和属性文件,且我们不会主动修改这些文件。托管对象类和属性文件在编译时被放到编译目录,而不会出现在工程列表中;
Category/Extension 由Core Data自动生成托管对象类和属性文件,同时我们可自己生成分类,添加自己的业务逻辑或方法,以便能完全控制托管对象类的实现。
Manual/None Core Data 不会帮我们生成任何文件,由你自己手动维护类和属性。通过编辑面板中设置的class name来定位和关联这些文件。

6.Renaming ID

给实体重命名后,需要将新model中实体的renaming identifier设置成原model中实体的名字。

四.Attribute-属性

配置属性时至少需要指定属性名、类型、是否必须保存到存储中、保存时是否必须有值。

对于一些属性,你还可以选择是否使用纯量类型、属性的默认值、指定数据校验规则。

属性配置面板

1.Type

属性的可用数据类型如下所示:

数据类型

其中transformable用于保存我们自定义的类、数组及图片等非常规数据类型。

2.Optional

属性默认为Optional,即保存到持久化存储时,属性不要求必须有值。Core Data 不推荐使用这个选项,因为SQL中对Null的比较与OC中对nil的比较机制完全不一样,在数据库中NULL ≠ 0,且NULL ≠ 空字符串,所以用SQL搜索数字0的结果与搜索NULL的结果并不相同。

3.Default Value

属性默认值,初始化实体时,该属性会被自动赋上此默认值。可勾掉Optional选项搭配使用。

4.Transient

默认情况下,属性会被保存到持久化存储中,但Transient属性不会。短暂的属性适用于临时保存一些值的场景。Core Data 不会跟踪这种属性的变化,所以也就没法做回滚这种操作。

5.Validation

设置校验规则,例如给数值类型设置最大值最小值,或者给字符串类型设置正则表达式。

6.Renaming ID

给属性重命名后,需要将新model中属性的renaming identifier设置成原model中属性的名字。

五.Relationship-关系

设置关系时需要指定其名字、目标实体、删除规则、对1或对N类型,并且配置反相的关系。

关系配置

1.Destination

目标实体,例如一个课程对应多个老师时,在课程实体中可设置关系的目标实体为名老师

2.Inverse

设置关系的另一半,因为在面板中只能从一个方向定义关系,这个选项是让两个关系组合起来成为一个完整的双向关系。这样 Core Data 才能在实体发生变化时在双方间传递这种变化。

3.Delete Rule

当源实体被删除时,关系的目标实体如何响应。

选项
删除规则
No Action 删除源实体时,目标实体中保留关系的引用,由你手动更新
Nullify 删除源实体时,目标实体中的关系引用自动置空
Cascade 删除源实体时,同时删除关系中的所有目标实体
Deny 只在关系未指向任何目标实体时才删除源实体
  • No Action

无操作,删除A后其关联的B没任何操作,不会将B中关联属性(A)指向nil。删除A后使用B的关联属性(A),可能会导致其他问题,所以一般不推荐使用此配置。

  • Nullify

翻译为作废,默认选项,当A对象被删除时,B中指向的对象A会被置空,B本身不受影响,所以删除A不会删除B。例如,删除一个部门时,把员工对应的部门字段置为nil。

  • Cascade

级联,当A对象被删除时,A对象包含的对象B也会被删除。一般用在 1-N 的关系中,1 的一方被删除,则 N 的一方随之被全部级联删除。相反,在 N-1 的关系中,则一般不能使用级联关系,删除多的一方时一定不能直接级联删除一的一方。例如,删除了部门以后,所有的员工对象都要被删除。

  • Deny

拒绝,删除当前对象时,如果当前对象还指向其他关联对象,则当前对象不能被删除。例如,删除部门时,如果还有一个员工,删除操作就会被拒绝。

4.Cardinality Type

关系分为对1对多两种类型。

5.Renaming ID

给关系重命名后,需要将新model中关系的renaming identifier设置成原model中关系的名字。

六.Core Data Stack

创建数据模型文件后需要设置相关类以便真正操作数据对象,这些类被称为Core Data Stack

Core Data Stack

1.MOM

ManagedObjectModel,托管对象模型,对应着Xcode中的.xcdatamodeld文件,保存在工程或 framework 里,通过URL加载。

1
2
3
4
5
6
7
8
- (NSManagedObjectModel *)managedObjectModel {
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _managedObjectModel;
}

MOM中包含了一个或多个NSEntityDescription对象,维护着Entity与对应的managed object间的映射关系。MOM支持使用预定义的Fetch request,以返回符合条件的数据对象。可以在.xcdatamodeld编辑面板中定义 fetch request 模板,也可以通过代码创建。

1
2
3
4
NSManagedObjectModel *model = [CoreDataStack shareInstance].managedObjectModel;
NSFetchRequest *request = [model fetchRequestTemplateForName:@"Fetch1"];
NSError *error;
NSArray *matchArr = [self.viewContext executeFetchRequest:request error:&error];

2.MOC

ManagedObjectContext,托管对象上下文,在managed object的生命周期中扮演者重要角色。

1
2
3
4
5
6
7
8
9
10
11
12
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
}
  • 跟踪变化

所有的托管对象都必须在MOC中,而 MOC 位于RAM中。通过 MOC 你可以从持久化存储中返回托管对象,并且 MOC 会负责跟踪它们的变化,例如对托管对象的增删改、校验、反向关系的处理、回滚/重做等。这些变化会先保存在内存中关联的 MOC 中,直到执行 save 操作时,Core Data 才会将此变化同步到持久化存储中。

1
2
3
4
5
6
7
8
9
10
11
12
// 1.从数据库中删除
NSFetchRequest *fetchRequest = [Course fetchRequest];
Course *indexedCourse = _mDatasourceArr[index];
NSPredicate *predicate= [NSPredicate predicateWithFormat:@"id = %d",indexedCourse.id];
[fetchRequest setPredicate:predicate];

NSError *error = nil;
NSArray *fetchedObjects = [self.viewContext executeFetchRequest:fetchRequest error:&error];
for (Course *c in fetchedObjects) {
[self.viewContext deleteObject:c]; // 还在内存中
}
[self save]; // 写入持久化存储中
  • 为什么要有MOC这一层呢?

我们的数据对象需要保存到持久化存储中,而持久化存储一般在磁盘中,读写速度相对较慢,不应频繁地访问。MOC位于 RAM 中,读写速度相对较快,它可以快速访问内存中的托管对象,跟踪托管对象的频繁变化,提供完整的回滚和重做支持。开发者只需定期通过 MOC 调用save方法,将这些托管对象真正写入磁盘中。

3.PSC

NSPersistentStoreCoordinator,是MOC与应用持久化存储之间的桥梁。MOC需要通过PSC访问MOM,PSC将注册在其下面的持久化存储集中展示,便于MOC一次性操作,而非一个个去操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"ASDF.sqlite"];
NSError *error = nil;
// 数据库做轻量迁移时 传入此options字典
NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption: @YES,
NSInferMappingModelAutomaticallyOption: @YES};

if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:options error:&error]) {
}
return _persistentStoreCoordinator;
}

PSC在私有串行队列上执行任务,如果有需要你也可以使用多个PSC以便在不同的队列上执行任务。

使用PSC可以增加或删除某个持久化存储,更改存储的类型或位置,查询存储的元数据,定义存储的迁移,定义两个对象是否源自于同一个存储等等。

4.NSPersistentContainer

这是 iOS10 之后新出的一个封装了MOC、MOM、PSC的容器,用来简化 Core Data stack 的初始化和管理工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSPersistentContainer *)persistentContainer
{
@synchronized (self) {
if (_persistentContainer == nil) {
_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"Model"]; // 只需指定model文件名
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
if (error != nil) {
NSLog(@"error %@, %@", error, error.userInfo);
}
}];
}
}
return _persistentContainer;
}

它提供了我们常用的三个 Stack 属性,通过 persistentContainer 对象调用即可:

1
2
3
@property (strong, readonly) NSManagedObjectContext *viewContext;
@property (strong, readonly) NSManagedObjectModel *managedObjectModel;
@property (strong, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

七.托管对象

定义好MOM、初始化 Core Data Stack 之后,就可以为持久化存储创建、保存数据对象了。

1.MO

NSManagedObject,表示持久化存储中保存的数据对象,它是所有MO对象的基类,是Entity在代码层面真正对应的类。在.xcdatamodeld编辑界面中,选中Course这个Entity,打开右侧的属性面板,在Entity栏下有个Name字段,它是Entity.xcdatamodeld文件中的名字;而下方有个Class栏,这些是与此Entity绑定的MO子类信息,其中Name字段就是对应的MO子类名,一般与Entity同名,即Course。当然,你也可以根据自己的规范定义成别的名字。

1
2
@interface Course : NSManagedObject
@end

通常Entity属性面板中codegen字段设置为Class Definition时,Core Data 会自动为我们生成此类定义,只是这个类不会出现在代码列表中,我们可在代码中导入其头文件直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1.导入MO实体头文件
#import "Course+CoreDataClass.h"

// 2.创建 NSManagedObject 对象
Course *c = [NSEntityDescription insertNewObjectForEntityForName:@"Course" inManagedObjectContext:self.viewContext];
c.id = index;
c.name = @"英语"; // 给实体的属性赋值
NSManagedObjectContext *context = self.persistentContainer.viewContext;
NSError *error = nil;

// 3.保存到持久化存储中
if ([context hasChanges] && ![context save:&error]) {
NSLog(@"~~error %@, %@", error, error.userInfo);
}

2.NSEntityDescription

通过上面MO的示例可以看到,使用MO时需要两个元素的配合:NSEntityDescriptionMOC

其中NSEntityDescription是对模型文件中Entity的描述,包括名字、属性、关系,及代码层面此Entity代表的MO实体类。我们在.xcdatamodeld编辑面板中定义Entity,而 Core Data 使用NSEntityDescription来匹配持久化存储中的Managed Object

后者是管理MO的上下文,顾名思义,就是用来跟踪MO及其关系的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 设置描述
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *desription = [NSEntityDescription entityForName:@"Course" inManagedObjectContext:self.viewContext];
[fetchRequest setEntity:desription];

Course *indexedCourse = _mDatasourceArr[index];
NSPredicate *predicate= [NSPredicate predicateWithFormat:@"id = %d",indexedCourse.id];
[fetchRequest setPredicate:predicate];

// 根据描述查询 Managed Object
NSError *error = nil;
NSArray *fetchedObjects = [self.viewContext executeFetchRequest:fetchRequest error:&error];
for (Course *c in fetchedObjects) {
// 删除 Managed Object
[self.viewContext deleteObject:c];
}
[self save];

八.增删改查

1.查询

1.1.结果类型

MOC需通过NSFetchRequest执行查询操作,查询结果以数组形式返回,默认情况下数组中返回的是托管对象MO。当然你也可以通过 fetch 的resultType属性将数组中的对象指定为其他类型。

1
@property (nonatomic) NSFetchRequestResultType resultType;

Core Data 支持四种返回类型:

1
2
3
4
5
6
typedef NS_OPTIONS(NSUInteger, NSFetchRequestResultType) {
NSManagedObjectResultType = 0x00,
NSManagedObjectIDResultType = 0x01,
NSDictionaryResultType = 0x02,
NSCountResultType = 0x04
};
  • NSDictionaryResultType,返回字典类型的对象;
  • NSCountResultType,返回请求结果的count数值;
  • NSManagedObjectResultType,默认值,查询结果数组中的元素为MO对象;
  • NSManagedObjectIDResultType,查询结果数组中的元素为NSManagedObjectID 对象,即MO对象的ID。这种类型的内存占用比较小,MOC可以继续通过ID对象获取对应的MO。
1
2
NSManagedObjectID *moID = objectIDs[0];
NSManagedObject *obj = [managedObjectContext existingObjectWithID:moID error:&error];
1.2.Fault对象
1
@property (nonatomic) BOOL returnsObjectsAsFaults;

这是 Fetch 请求的一个属性,默认值是YES,表示让查询结果返回Fault(惰值)状态的托管对象。Fault对象是托管对象的占位对象,作为查询结果被保存在 MOC 中,而 MOC 位于内存中。为了节省内存开销,Fault对象的属性值暂时不会填充到对应字段上,而是先保存在持久化存储的row cache(行缓存)中。当访问或修改这些属性值时,Core Data 才去持久化存储的行缓存中取出属性值并填充到Fault对象中,使其成为完全体的托管对象。

这种设计减少了内存消耗,却有一定的性能开销,如果你想在 fetch 结果返回后立即访问其中MO对象的属性,则应该将这个属性的值置为NO

另外,这个属性只在返回类型为ObjectResultType时有效,对ObjectIDResultType无效, 因为 object IDs 没有属性值,设置此属性没有意义。

1.3.过滤条件
1
@property (nonatomic, strong) NSPredicate *predicate;
运算符
作用
示例
> 、< 、== 、>= 、<= 、!= 比较运算 age > 18
IN 被包含 name IN {‘张三’,’李四’}
BETWEEN 在区间内 age BETWEEN {18,80}
BEGINSWITH 开头是 name BEGINSWITH ‘张’
ENDSWITH 结尾是 name ENDSWITH ‘四’
CONTAINS 包含有 name CONTAINS ‘四’
LIKE 通配符 *和? name LIKE ‘*四’
MATCHES 正则 name MATCHES ‘(regex)’
1.4.排序规则

返回数据的排序规则,这是个数组,即支持多重排序,按数组中的前后顺序先后执行。

1
@property (nonatomic, strong) NSArray<NSSortDescriptor *> *sortDescriptors;
1.5.分页查询
1
2
3
4
5
6
// 总共查询的数量
@property (nonatomic) NSUInteger fetchLimit;
// 每次查询返回的数量
@property (nonatomic) NSUInteger fetchBatchSize;
// 分页查询的下标,从哪条开始查
@property (nonatomic) NSUInteger fetchOffset;
1.6.案例演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)loadDataAtPage:(NSUInteger)page{
// 1.设置请求
NSFetchRequest *request = [Course fetchRequest];
/*相当于上面的代码
NSFetchRequest * request = [[NSFetchRequest alloc] init];
NSEntityDescription *desription = [NSEntityDescription entityForName:@"Course" inManagedObjectContext:self.viewContext];
[request setEntity:desription];
*/
// 2.排序
NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:@"id" ascending:YES];
request.sortDescriptors = @[descriptor];

// 3.分页
request.fetchOffset = (page - 1) * kLimiteSize;
request.fetchLimit = kLimiteSize;

// 4.查询
NSError *error;
NSArray *matchArr = [self.viewContext executeFetchRequest:request error:&error];
}

2.修改

先查询MO对象,再逐一修改其属性,最后由MOC执行save,保存到持久化存储中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)updateDataAtIndex:(NSUInteger)index{
// 1.创建fetch
NSFetchRequest *request = [Course fetchRequest];
// 2.设置过滤条件
NSPredicate *predicate= [NSPredicate predicateWithFormat:@"id = %d",index];
[request setPredicate:predicate];
NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:@"id" ascending:YES];
request.sortDescriptors = @[descriptor]; // 排序
// 3.执行fetch
NSError *error;
NSArray *matchArr = [self.viewContext executeFetchRequest:request error:&error];
NSInteger count = matchArr.count;
for (NSInteger i = 0; i < count; i++) {
Course *c = matchArr[i];
//4.修改托管对象
c.name = [c.name stringByAppendingString:@"X"]; //managed object对象,更新到内存中
}
// 5.同步到数据库
NSError *error = nil;
if ([self.viewContext hasChanges] && ![self.viewContext save:&error]) {
NSLog(@"~~error %@, %@", error, error.userInfo);
}
}

3.插入

插入数据,要用到以下这个NSEntityDescription的静态方法来执行数据的创建工作:

1
2
+ (NSManagedObject *)insertNewObjectForEntityForName:(NSString *)entityName
inManagedObjectContext:(NSManagedObjectContext *)context;

这里的name参数是模型文件中Entity的名字,Core Data 根据Entity的结构来创建MO对象。

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
// 往数据库中插入数据
- (void)insertData{
// 1.在内存中创建MO实例并设置各字段
int32_t index = (int32_t)self.mDatasourceArr.count;
Student *s1 = [[Student alloc] init];
s1.sID = 101;
s1.name = @"学生甲";

Student *s2 = [[Student alloc] init];
s2.sID = 102;
s2.name = @"学生乙";

Teacher *t = [NSEntityDescription insertNewObjectForEntityForName:@"Teacher" inManagedObjectContext:self.viewContext];
t.id = index;
t.name = [NSString stringWithFormat:@"老师%d",index];
t.avator = [UIImage imageNamed:@"1"];
t.descript = @"省级优秀教师";

Course *c = [NSEntityDescription insertNewObjectForEntityForName:@"Course" inManagedObjectContext:self.viewContext];
c.id = index;
c.name = [NSString stringWithFormat:@"课程%d",index];
c.url = @"https://www.baidu.com";
c.teachers = [NSSet setWithObject:t]; // 1->N,直接设置course对应的关系“teachers”字段
c.students = [NSSet setWithObjects:s1, s2, nil]; // 数组,transformable类型,数组中的元素student需要实现NSCoding协议

// 2.通过MOC执行保存操作,写入数据库
NSError *error = nil;
if ([self.viewContext hasChanges] && ![self.viewContext save:&error]) {
NSLog(@"~~error %@, %@", error, error.userInfo);
}

4.删除

先查询MO对象,再通过MOC执行delete,删除这些MO,最后执行save同步到存储中。

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
- (void)removeDataAtIndex:(NSUInteger)index{
// 1.设置查询目标实体及条件
NSFetchRequest *fetchRequest = [Course fetchRequest];
/*相当于上面的代码
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *desription = [NSEntityDescription entityForName:@"Course" inManagedObjectContext:self.viewContext];
[fetchRequest setEntity:desription];
*/
Course *indexedCourse = _mDatasourceArr[index];
NSPredicate *predicate= [NSPredicate predicateWithFormat:@"id = %d",indexedCourse.id];
[fetchRequest setPredicate:predicate];

// 2.从数据库中查询MO
NSError *error = nil;
NSArray *fetchedObjects = [self.viewContext executeFetchRequest:fetchRequest error:&error];
for (Course *c in fetchedObjects) {
// 3.在内存中删除MO
[self.viewContext deleteObject:c];
}
// 4.在数据库中删除MO
NSError *error = nil;
if ([self.viewContext hasChanges] && ![self.viewContext save:&error]) {
NSLog(@"~~error %@, %@", error, error.userInfo);
}
}

九.类型转换

在定义Entity时,属性可以是以下类型:

  • Integer
  • Float
  • Double
  • Decimal
  • Boolean
  • String
  • Date
  • Binary Data
  • UUID
  • URI
  • Transformable

Core Data 不支持直接保存图片、音视频文件、颜色、数组、自定义类型等,这时可以将实体中对应的属性设置成 Binary Data 类型,再将这些文件或自定义类型转换成 Data,赋值给实体的属性,执行 save 保存到持久化存储中。读取属性时则反过来,将持久化存储中的 data 手动转换成对应的文件或自定义类型即可。

但是,每次都自己手动转换显然比较麻烦。这时可以这样做:

  1. 将属性设置成Transformable类型;
  2. 提供一个继承自NSValueTransformer的子类,重写必须的方法;
  3. 在模型编辑窗口,设置属性的TransformerCustom Class字段;
  4. 保存属性时,直接给属性赋值,Core Data 会通过我们指定的NSValueTransformer子类,自动执行数据转换,将属性中的值转换成 data,写入持久化存储;
  5. 读取属性时,从持久化存储中读取 data,由NSValueTransformer子类自动转换成对应类型;

这里的NSValueTransformer是一个抽象类,支持单向或双向的类型转换。其内部已经封装好了数据类型转换所需的抽象方法,子类需要自己提供实现:

1
+ (Class)transformedValueClass;
  • 转换后对象所属的类。

例如将Student保存入库时,需要将Student对象转换成Data,这里就返回Data类型。

1
- (nullable id)transformedValue:(nullable id)value;
  • 被转换对象的值。

例如保存Student类型时,需要将Student对象转换成Data,这里就返回转换之后 Data 的值。

1
+ (BOOL)allowsReverseTransformation;
  • bool值,表示是否支持反向转换。

例如将Student转成data入库后,读取时是否允许将data回转成Student

1
- (nullable id)reverseTransformedValue:(nullable id)value;
  • 反向转换时,转换出来的对象的值。

例如读取属性时,要将库中的Data转换成Student,这里就返回转换之后的Student实例。


下面来看一些常见的类型转换怎么重写:

1.颜色转Data

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
32
33
34
35
36
37
// 颜色转Data
@interface RGBColorValueTransformer : NSValueTransformer
@end

@implementation RGBColorValueTransformer

// 颜色转Data,这里返回 Data 类型
+ (Class)transformedValueClass {
return [NSData class];
}

// 颜色转Data后得到的data
- (id)transformedValue:(id)value {
UIColor* color = value;
const CGFloat* components = CGColorGetComponents(color.CGColor);
NSString* colorAsString = [NSString stringWithFormat:@"%f,%f,%f,%f", components[0], components[1], components[2], components[3]];
return [colorAsString dataUsingEncoding:NSUTF8StringEncoding];
}

// Data反向转换成颜色时得到的颜色对象
- (id)reverseTransformedValue:(id)value {
NSString* colorAsString = [[NSString alloc] initWithData:value encoding:NSUTF8StringEncoding];
NSArray* components = [colorAsString componentsSeparatedByString:@","];
CGFloat r = [[components objectAtIndex:0] floatValue];
CGFloat g = [[components objectAtIndex:1] floatValue];
CGFloat b = [[components objectAtIndex:2] floatValue];
CGFloat a = [[components objectAtIndex:3] floatValue];

return [UIColor colorWithRed:r green:g blue:b alpha:a];
}

// 是否允许反向转换
+ (BOOL)allowsReverseTransformation {
return YES;
}

@end

2.图片转Data

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
// 图片转Data
@interface ImageToDataValueTransformer : NSValueTransformer
@end

@implementation ImageToDataValueTransformer

// 图片转Data时,data所属的类
+ (Class)transformedValueClass {
return [NSData class];
}

// 图片转Data后,得到的data
- (id)transformedValue:(id)value {
if (value == nil) {
return nil;
}
return UIImagePNGRepresentation(value);
}

// data反向转成图片时,得到的图片对象
- (id)reverseTransformedValue:(id)value {
return [UIImage imageWithData:(NSData *)value];
}

// 是否允许反向转换
+ (BOOL)allowsReverseTransformation {
return YES;
}

@end

3.数组转Data

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
32
// 数组转Data
@interface ArrayToDataValueTransformer : NSValueTransformer
@end

@implementation ArrayToDataValueTransformer

// 数组转Data时,被转换的数组所属的类
+ (Class)transformedValueClass{
return [NSArray class];
}

// 数组转换成Data时,得到的data
- (id)transformedValue:(id)value{
if (value == nil) {
return nil;
}
return [NSKeyedArchiver archivedDataWithRootObject:value requiringSecureCoding:YES error:nil];
}

// data反过来转换成数组时,得到的数组对象
- (id)reverseTransformedValue:(id)value{
// 指明转换成数组后,其中元素的类型
NSSet *unarchivedSet = [NSSet setWithObjects:[MyClassA class], [MyClassB class], nil];
return [NSKeyedUnarchiver unarchivedObjectOfClasses:unarchivedSet fromData:value error:nil];
}

// 是否允许反向转换
+ (BOOL)allowsReverseTransformation{
return YES;
}

@end

注意,当被转换的是自定义类型时,需要这些自定义类型实现NSSecureCoding 协议,重写encodeWithCoder:initWithCoder:方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementation MyClassA

- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_property1 forKey:@"property1"];
[aCoder encodeObject:_property2 forKey:@"property2"];
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init])
{
_property1 = [aDecoder decodeObjectForKey:@"property1"];
_property2 = [aDecoder decodeObjectForKey:@"property2"];
return self;
}
return nil;
}

+ (BOOL)supportsSecureCoding{
return YES;
}

@end

十.性能优化

IO操作通常是比较费性能的,而 Core Data 的底层就是对 sqlite 文件等持久化存储进行读写,大量的读写操作会阻塞线程或引发性能问题,所以要考虑内存、性能、线程、并发等问题。

1.多MOC

1.1.并发

为了缓解主线程的压力,对于一些不涉及到UI更新的数据库操作,通常是放到新开辟的一个或多个线程进行。需要注意的是,Core Data的MO与MOC不是线程安全的,对MO与MOC的操作不会上锁去保证操作的原子性,多个线程共用MOC时可能会出现数据混乱甚至闪退。

多个线程共用一个MOC

错误示范:

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
32
33
34
35
36
37
38
39
40
41
// 测试:多个线程中共用一个MOC可能会引起数据混乱或闪退
- (void)testOptionsInMultiThread {

// 1.共用一个MOC
_sameContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_sameContext.persistentStoreCoordinator = self.persistentStoreCoordinator;

// 2.模拟测试数据
NSMutableArray *sectionData = [NSMutableArray array];
for (int i = 0; i < 30; i++) {
NSString *descript = [NSString stringWithFormat:@"this is %d",i];
[sectionData addObject:@{@"id": @(i), @"name": @(i), @"descript": descript}];
}
// 3.异步线程A 插入数据
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSDictionary *params in sectionData) {
Section *section = [NSEntityDescription insertNewObjectForEntityForName:@"Section" inManagedObjectContext:_sameContext];
int idf = [params[@"id"] intValue];
section.id = idf;
section.name = idf;
section.descript = params[@"descript"];
// 模拟插入10条数据后,异步线程B执行更新操作
if (idf == 10) {
sleep(1);
}
}
[_sameContext save:nil];
});
// 4.异步线程B 更新数据
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Section"];
NSArray *results = [_sameContext executeFetchRequest:fetchRequest error:nil];
for (Section *section in results) {
int i = arc4random();
section.id = i;
section.name = i;
section.descript = [NSString stringWithFormat:@"this is %d",i];
}
[_sameContext save:nil];
});
}

示例中线程A线程B分别执行插入和更新操作。由于是共用一个MOC且是异步操作,线程A新增的MO可能在线程B中被提前执行了save,两种操作混在一起会产生闪退。

苹果推荐的是每个线程使用一个独立的MOC,这样MOC在自己所属的线程中管理自己监听的MO,不受其他MOC的影响,从而避免MOC保存前它监听的MO被其他MOC篡改或提前 save 的情况。

多个线程对应多个MOC

创建多MOC时需要指定它的并发类型以便进行多线程管理,Core Data 提供了两个选择:

  1. Main:MOC与主队列关联并且依赖应用的event loop
  2. Private:MOC会创建并管理一个私有串行队列。
1
2
3
4
5
6
// 主MOC
_mainContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSMainQueueConcurrencyType];
// 私有队列MOC
_backgroudnContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];

基于队列的多 MOC 需要搭配以下两个API使用:

1
2
[moc performBlock:]
[moc performBlockAndWait:]

前者是异步操作,被调用后立刻返回;

后者是同步操作,会堵塞线程直到任务完成才返回。

对于更新UI或其他需要在主线程中执行的操作,推荐使用Main主队列MOC。

对于一些耗时的任务,推荐使用Private私有队列MOC+异步block执行。

1.2.同PSC

示例1:多MOC使用同一个PSC

多MOC使用同一个PSC

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
- (void)initMulMOCWithOnePSC
{
// 主队列使用的MOC
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainContext.persistentStoreCoordinator = self.persistentStoreCoordinator;

// 私有队列MOC,处理耗时任务
_backgroudnContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroudnContext.persistentStoreCoordinator = self.persistentStoreCoordinator;

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUpdate:) name:NSManagedObjectContextDidSaveNotification object:_mainContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUpdate:) name:NSManagedObjectContextDidSaveNotification object:_backgroudnContext];

// 主MOC增加实体
Course *c = [NSEntityDescription insertNewObjectForEntityForName:@"Course" inManagedObjectContext:self.mainContext];
c.id = 1;
c.name =@"A1";
[self.mainContext save:nil];

// 子MOC增加实体
[_backgroudnContext performBlock:^{
// 子MOC插入实体
Course *c2 = [NSEntityDescription insertNewObjectForEntityForName:@"Course" inManagedObjectContext:self.backgroudnContext];
c2.id = 2;
c2.name =@"A2";
// 子MOC执行save
[self.backgroudnContext save:nil];
}];
}

// 监听save通知,合并来自其他MOC的修改
- (void)onUpdate:(NSNotification *)notification{

NSLog(@"++++通知所在线程:%@",[NSThread currentThread]);

// 区分通知中context是哪个,将其中的变化合并到别的context中
NSManagedObjectContext *context = notification.object;
if ([context isEqual:_mainContext]) {
[_backgroudnContext performBlock:^{
NSLog(@"++++_backgroudnContext performBlock 所在线程:%@",[NSThread currentThread]);
[_backgroudnContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
else if ([context isEqual:_backgroudnContext]){
[_mainContext performBlock:^{
NSLog(@"++++_mainContext performBlock 所在线程:%@",[NSThread currentThread]);
[_mainContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
}

不要在一个线程上创建MO再把它传给另一个线程,可通过MOC使用MO的ID查询对应的MO;或者监听NSManagedObjectContextDidSaveNotification通知,在回调里合并来自其他MOC的修改。

这种方案的问题在于,需要在不同的MOC间监听通知,手动同步来自其他MOC的修改,稍显麻烦。

1.3.父MOC

MOC都有对应的父存储,通过父存储可以返回代表托管对象的数据,也可以提交修改之后的托管对象。在 iOS5 之前,父存储只能是persistent store coordinator,而 iOS5 之后父存储可以是另一个MOC了。但无论如何,最终MOC的根源必须是一个PSC,通过PSC提供MOM并分发增删改查等请求到不同的持久化存储中。

父MOC这种模式适用于在子线程处理耗时任务的场景。例如,将主线程对应的MOC设置为子线程MOC的父存储,那么在子MOC中保存对MO的修改时,这些修改会被推送到父MOC中,最终子线程MOC的 fetch 与 save 操作都会通过主线程MOC代为执行。与同PSC方案相比,此方案里某一个MOC的改动不需要用通知去告知其他MOC同步了,省事不少。

注意:保存对子MOC中数据的修改时,切记先在子MOC中执行 save,再在父MOC中执行 save。

示例2:子MOC使用 parent context

父子MOC

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (void)initMulMocWithParentMoc
{
// 主队列使用的MOC
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContextWillSave:) name:NSManagedObjectContextWillSaveNotification object:_mainContext];

// 私有队列MOC,处理耗时任务
_backgroudnContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroudnContext.parentContext = _mainContext; // 设置 parent store
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContextWillSave:) name:NSManagedObjectContextWillSaveNotification object:_backgroudnContext];

// 在父MOC中增加一个实体
Course *c = [NSEntityDescription insertNewObjectForEntityForName:@"Course" inManagedObjectContext:self.mainContext];
c.id = 1;
c.name =@"A1";
[self.mainContext save:nil];

// 在子MOC中执行perform
[self.backgroudnContext performBlock:^{

// 在子MOC中增加一个实体
Course *c2 = [NSEntityDescription insertNewObjectForEntityForName:@"Course" inManagedObjectContext:self.backgroudnContext];
c2.id = 2;
c2.name =@"A2";

// 先在子MOC中执行 save
[self.backgroudnContext save:nil];

[self.mainContext performBlock:^{
// 再在父MOC中执行 save,否则backgroudnContext所做修改不会被持久化
[self.mainContext save:nil];
}];
}];
}

// 监听通知,将新创建的MO的临时ID转换成永久ID
- (void)onContextWillSave:(NSNotification *)notification{
NSManagedObjectContext *moc = notification.object;
NSSet *insertMO = moc.insertedObjects;
if (insertMO.count) {
BOOL succeed = [moc obtainPermanentIDsForObjects:insertMO.allObjects error:nil];
if (!succeed) {
NSLog(@"Error occured!");
}
}
}

必要时可以使用三MOC,其中:

  • Private MOC用于执行耗时操作;
  • Main MOC用于与UI协作;
  • Root MOC用于在后台保存所有子MOC提交的修改。

三MOC方案

示例3:使用三MOC

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
- (void)init3MocWithParentMoc
{
// 根MOC,保存子MOC提交的修改
_rootContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_rootContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContextWillSave:) name:NSManagedObjectContextWillSaveNotification object:_rootContext];

// 主队列使用的MOC
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainContext.parentContext = _rootContext;

// 私有队列MOC,处理耗时任务
_backgroudnContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroudnContext.parentContext = _mainContext;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContextWillSave:) name:NSManagedObjectContextWillSaveNotification object:_backgroudnContext];

// 在子MOC中执行perform
[self.backgroudnContext performBlock:^{
// 1.在子MOC中增加一个实体
Course *c2 = [NSEntityDescription insertNewObjectForEntityForName:@"Course" inManagedObjectContext:self.backgroudnContext];
c2.id = 2;
c2.name =@"A2";

// 2.先在子MOC中执行 save
[self.backgroudnContext save:nil];

[self.mainContext performBlock:^{
// 3.再在父MOC中执行 save
[self.mainContext save:nil];

[self.rootContext performBlock:^{
// 4.最后在rootMOC中执行 save,保存子MOC提交的修改
[self.rootContext save:nil];
}];
}];
}];
}

// 监听通知,将新创建的MO的临时ID转换成永久ID
- (void)onContextWillSave:(NSNotification *)notification{
NSManagedObjectContext *moc = notification.object;
NSSet *insertMO = moc.insertedObjects;
if (insertMO.count) {
BOOL succeed = [moc obtainPermanentIDsForObjects:insertMO.allObjects error:nil];
if (!succeed) {
NSLog(@"Error occured!");
}
}
}

使用多MOC时需要注意:MO在实例化时会被赋予一个临时ID,这个ID在当前MOC范围内是唯一的。但在提交对MOC的修改时,要将临时ID转换成全局ID,所以需要监听MOC即将保存的通知,以转换永久ID。

1.4.通知

MOC会在不同的时机发送不同的通知,注册通知时需要区分MOC,一是因为我们自己会定义不同的MOC,二是系统本身也会使用 Core Data 并发送通知。我们只关心从自己定义的特定MOC中收到的通知,所以在注册通知时需要做区分,可参考上面的👆🏻示例1

1
2
3
4
5
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(<#Selector name#>)
name:NSManagedObjectContextDidSaveNotification
object:<#A managed object context#>];

2.批量操作

2.1.一般流程

先来看一次普通查询或修改对象属性值操作的执行流程:

  1. 执行查询时,MOC 将fetchRequest传递给 PSC;
  2. PSC 将 fetch 请求转换成对应的NSPersistentStoreRequest,并执行自己的excute方法,将 fetch 与 MOC 发送给持久化存储(NSPersistentStore);
  3. 持久化存储将NSPersistentStoreRequest转换成 SQL 语句,交给 SQLite 执行;
  4. SQLite 将符合条件的数据返回给持久化存储,持久化存储将其保存在行缓存(row cache)中;
  5. 持久化存储将获取到的数据实例化成托管对象,交给PSC。此时 fetch.returnsObjectsAsFaults 的默认值为YES,所以这些对象暂时还是惰值形态(Fault)的,其属性值尚未填充,而是暂时被保存在了持久化存储的行缓存中;
  6. PSC将这些Fault形态的托管对象以数组的形式返回给MOC;
  7. 访问或修改这些托管对象时,MOC会检查它们是否为Fault形态。如果是,则MOC 向 PSC 发起填充请求;
  8. PSC 向持久化存储请求与当前对象关联的数据;
  9. 持久化存储在它的行缓存中查找并返回数据,交给MOC;
  10. MOC将返回的数据填充到Fault形态的托管对象中,使其成为完全体的托管对象;
  11. 执行 save 时 MOC 发送NSManagedObjectContextWillSaveNotification通知;
  12. 创建一个持久化存储请求(NSSaveChangesRequest),调用PSC的 excute 方法,将请求发送给持久化存储;
  13. 持久化存储对比请求中的数据与自己行缓存中的数据,检测是否有冲突并按照设置的合并策略处理冲突;
  14. 持久化存储将NSSaveChangesRequest转换成 SQL 语句交给 SQLite 执行更新;
  15. 持久化存储更新自己的行缓存
  16. MOC 发送NSManagedObjectContextDidSaveNotification通知;

可以看到,整个过程所需的步骤还是挺多的~

2.2.批量操作

前面介绍的增、删、改,都是先从持久化存储中读取数据对象到内存中,再通过 MOC 逐一对这些对象执行增删改,最后执行 save 将数据再次保存到持久化存储中。在涉及到大批量数据的操作时,这种方式效率低、费内存,可能会遇到性能问题。为此,Core Data 提供了批量操作功能。

批量操作是直接在持久化存储中操作 MO 对象,不需要先查询 MO 对象,效率高;不需查询 MO 也就无需将它们加载到内存中,节省了内存开销。这个功能需要用到NSBatchxxxRequest系列类,并且这些类只支持 SQLite 类型的持久化存储,因为它是在持久化存储的 SQL 层面直接操作这些对象。也正是因为发生在 SQL 层面,所以 MOC 不会自动合并这种操作,也不会发送相关通知,需要我们自己通过 MOC 的 merge 静态方法,同步增删改内存中对应的 MO 对象。为此,需要将批量操作的返回值设置成NSManagedObjectID,即被操作对象的ID,再用这些ID更新 MOC 即可。

需要注意的是,在执行批量操作时,最好是放到一个私有 MOC 中进行。

2.3.批量修改
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
32
33
34
- (void)batchUpdate{

// 私有队列MOC,处理耗时任务
_backgroudnContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroudnContext.parentContext = _mainContext; // 设置 parent store

[_backgroudnContext performBlock:^{
// 1.设置修改的实体
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:@"Course"];
NSPredicate *predicate= [NSPredicate predicateWithFormat:@"id > 0"];
[request setPredicate:predicate];
// 2.设置修改的字段
request.propertiesToUpdate = @{@"name":@"课程S",@"url":@"www.xxx.com"};
request.resultType = NSUpdatedObjectIDsResultType;

// 3.执行批量修改
NSError *error;
NSBatchUpdateResult *batchResult = [self.viewContext executeRequest:request error:&error];
NSArray<NSManagedObjectID*> *updatedObjectIDs = batchResult.result;

// 同步数据的变化到MOC中 方式1(推荐)
NSDictionary *updatedDict = @{NSUpdatedObjectsKey : updatedObjectIDs};
[NSManagedObjectContext mergeChangesFromRemoteContextSave:updatedDict intoContexts:@[self.viewContext]];

// 同步数据的变化到MOC中 方式2
/*
[updatedObjectIDs enumerateObjectsUsingBlock:^(NSManagedObjectID *objID, NSUInteger idx, BOOL *stop) {
NSManagedObject *obj = [self.viewContext objectWithID:objID];
if (![obj isFault]) {
[self.viewContext refreshObject:obj mergeChanges:YES];
}
}];*/
}];
}
2.4.批量删除
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
- (void)batchDelete{
// 私有队列MOC,处理耗时任务
_backgroudnContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroudnContext.parentContext = _mainContext; // 设置 parent store

[_backgroudnContext performBlock:^{
// 1.创建fetch
NSFetchRequest *fetch = [Course fetchRequest];
// 2.设置过滤条件
fetch.predicate = [NSPredicate predicateWithFormat:@"id > 0"];

// 3.创建删除request
NSBatchDeleteRequest *delReqest = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fetch];
delReqest.resultType = NSBatchDeleteResultTypeObjectIDs;

// 4.开始批量删除
NSError *error;
NSBatchDeleteResult *deleteResult = [self.viewContext executeRequest:delReqest error:&error];
NSArray<NSManagedObjectID*> *deletedObjectIDs = deleteResult.result;

// 5.同步更新到MOC
NSDictionary *deletedDict = @{NSDeletedObjectsKey : deletedObjectIDs};
[NSManagedObjectContext mergeChangesFromRemoteContextSave:deletedDict intoContexts:@[self.viewContext]];
}];
}
2.5.批量插入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)batchInser{
// 私有队列MOC,处理耗时任务
_backgroudnContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroudnContext.parentContext = _mainContext; // 设置 parent store

[_backgroudnContext performBlock:^{
// 1.创建由实体Map组成的数组
NSArray *sectionsArr = @[@{@"name":@(001),@"id":@(1),@"descript":@"Section 1"},
@{@"name":@(002),@"id":@(2),@"descript":@"Section 2"},
@{@"name":@(003),@"id":@(3),@"descript":@"Section 3"}];
// 2.创建插入请求
NSBatchInsertRequest *request = [[NSBatchInsertRequest alloc] initWithEntityName:@"Section" objects:sectionsArr];
request.resultType = NSUpdatedObjectIDsResultType;

// 3.执行批量插入
NSError *error;
NSBatchInsertResult *batchResult = [self.viewContext executeRequest:request error:&error];
NSArray<NSManagedObjectID*> *insertedObjectIDs = batchResult.result;

// 4.同步数据的变化到MOC中
NSDictionary *updatedDict = @{NSInsertedObjectsKey : insertedObjectIDs};
[NSManagedObjectContext mergeChangesFromRemoteContextSave:updatedDict intoContexts:@[self.viewContext]];
}];
}

需要指出的是,虽然批量操作比普通增删改操作的效率高很多,但它的代价是放弃了很多细节的处理,比如批量操作不支持校验、不会发送通知、无法处理 Entity 间的关系等。

3.减少内存消耗

3.1.分页查询

查询时最好是限制总量和每页的数量,防止所有结果一次性加载到内存中;

1
2
3
4
5
6
7
8
// 总共查询的数量
@property (nonatomic) NSUInteger fetchLimit;
// 每次查询返回的数量
@property (nonatomic) NSUInteger fetchBatchSize;
// 分页查询的下标,从哪条开始查
@property (nonatomic) NSUInteger fetchOffset;
// 返回Fault对象,其属性值在行缓存中,默认值YES
@property (nonatomic) BOOL returnsObjectsAsFaults;

案例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)loadDataAtPage:(NSUInteger)page{
// 1.设置请求
NSFetchRequest *request = [Course fetchRequest];

// 2.排序
NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:@"id" ascending:YES];
request.sortDescriptors = @[descriptor];

// 3.分页
request.fetchOffset = (page - 1) * kLimiteSize;
request.fetchLimit = kLimiteSize;

// 4.查询
NSError *error;
NSArray *matchArr = [self.viewContext executeFetchRequest:request error:&error];
}
3.2.fault对象
  • 返回fault

查询后不会立刻访问其属性值时,可设置 fetch 请求返回fault对象,以节省内存;

1
2
3
4
5
6
7
8
- (void)faultMO{
// 1.设置请求
NSFetchRequest *request = [Course fetchRequest];
request.returnsObjectsAsFaults = YES;
// 2.查询
NSError *error;
NSArray *matchArr = [self.viewContext executeFetchRequest:request error:&error];
}
  • 重置成fault

不再需要的托管对象,让 MOC 调用下面的方法,清除对象的属性值,将其还原成fault状态。

1
2
- (void)refreshObject:(NSManagedObject *)object 
mergeChanges:(BOOL)flag;

其中flag参数为NO时,MOC会丢弃未保存的修改,当前托管对象变成fault对象;

flag为YES时,MOC会从持久化存储或缓存中重新加载MO的属性值,再更新成本地修改的值。

3.3.重置所有MO

调用MOC的reset方法,可以将MOC中所有的MO都清除。此时原来跟MOC关联的MO都会失效,你需要重置它们的引用,并重新执行 fetch。

3.4.遍历与销毁MO

当遍历大量托管对象时,需要使用autorelease pool来确保临时MO尽快销毁。

3.5.不查询属性值

执行 Fetch 时,Core Data 默认会查询对象的ID和属性值,填充行缓存,用ID创建fault对象并返回。如果你确定只想查询托管对象,而不会访问其属性,可设置fetch.includesPropertyValues = NO,此时 Core Data 只会查询对象的 ID 并返回fault对象,而不会查询属性值,也不会填充行缓存,这样就又省去了一部分内存开销。

当然,如果includesPropertyValues=NO而你又访问了fault对象的属性,那么 Core Data 在空的行缓存中查询不到数据,就会去持久化存储中重新查询并填充到fault对象中。

如果includesPropertyValues=YESresultTypemanagedObjectIDResultType类型,那么还是会查询属性,这会造成不必要的性能开销。因为返回值是 ID 类型,没有属性字段,这些被查询出来的属性根本就没有机会被展示到应用里。

4.BLOBs-二进制大文件

Binary Large Data Objects,指二进制大数据对象,例如图片、音频等。使用Core Data 保存这种对象时,需要选择 SQLite 作为持久化存储。因为其他存储要求将整个对象都加载到内存中,并且写入存储是原子性的,这会导致它们处理 BLOBs 的效率相对不高。

BLOB 通常是 Entity 的属性,例如员工实体的头像属性。可以为 BLOB 创建一个单独的照片实体,在它与员工实体间设置1对1关系,以替代原员工实体中的头像属性。这样做可以充分利用Fault对象节省内存的特点,查询员工的照片关系字段时,Core Data 暂时不会将关系属性值(即二进制的照片数据)加载到内存中,只有真正访问或修改此属性时,才填充到fault对象中。

更好的做法是将 BLOBs 保存到文件系统里,将其 URL 或路径保存在数据库中,需要用到时再通过 URL 或路径加载此文件。

十一.合并冲突

应用中一般会存在多个MOC,它们使用同一份持久化存储,执行不同任务以便优化性能。每个 MOC 都能独立执行 save 保存对托管对象的修改,其他 MOC 需要合并这些修改,以保证各处托管对象状态的一致性。当 MOC 合并对托管对象的修改时,如果另一个 MOC 也修改了同一个属性并且值不相同,就产生了冲突。这时就需要根据 MOC 的合并策略属性来处理冲突:

1
@property (strong) id mergePolicy;

以下是各个策略及其对应的处理:

合并策略
说明
NSErrorMergePolicy 默认的合并策略,产生冲突时返回错误;
NSMergeByPropertyStoreTrumpMergePolicy 只合并产生冲突的属性,用外部修改覆盖当前修改,其他未产生冲突的属性保持不变;
NSMergeByPropertyObjectTrumpMergePolicy 只合并产生冲突的属性,用当前修改覆盖外部修改,其他未产生冲突的属性保持不变;
NSOverwriteMergePolicy 将当前MOC的托管对象写入持久化存储;
NSRollbackMergePolicy 丢弃冲突中所有的修改,保持持久化存储中的版本不变;

十二.数据迁移

MOM用来描述持久化存储中数据的结构,Core Data 中只能使用 MOM 打开持久化存储,改变 MOM 中的任何一部分都会导致它与之前版本的存储产生冲突。例如在.xcdatamodel中增加新实体,修改实体的名字,增删属性等,那么老版本的持久化存储就不能使用了。需要通过数据迁移,将老版本存储中的数据迁移到新版本中。为了让 Core Data 知道如何迁移数据,有时我们需要主动提供一些信息,例如,创建一个mapping model映射文件。对于简单的变化,可以使用轻量级迁移功能。

使用轻量级迁移功能时,Core Data 会根据前后MOM的不同,自动推断出一个mapping model,无需我们自己创建。轻量级迁移在我们项目的初期可能会非常有用,因为这一时期我们可能会经常性的改动模型文件,而又不希望频繁重新生成测试数据。当然,这些小的改动只能是以下这些操作:

  • 增、删、重命名属性;
  • 给属性设置默认值;
  • 不可选属性变可选;
  • 可选属性变不可选并设置默认值;
  • 增、删、重命关系;
  • 修改关系成对1或对多、排序或不排序;
  • 增、删、重命名实体;
  • 创建新的父实体或子实体;
  • 在实体继承树上将属性移到别的实体中;
  • 将实体移出继承树;

1.建新model

做数据迁移时既需要新版本model文件,也需要新版本model文件,新版model文件设置方法如下:

  • Xcode 中选中.xcdatamodeld文件;
  • Editor -> add model version
  • 设置新版本文件名和它基于哪个版本;
  • 创建新模型文件;
  • 设置新xcdatamodel作为当前版本;

这样,就可以在新版本的模型文件中修改 Entity 了。

新建Model 2.xcdatamodel文件

2.修改Entity

重命名属性时,需要在设置面板中将目标版本属性的Renaming ID字段设置成原属性的名字。例如可以在版本2中将属性name重命名成name2,设置name2Renaming IDname;然后在版本3中可以继续将name重命名成name3,同样将name3Renaming ID设置为name。这样 Core Data 就能从版本1推断出到版本2,或者从版本1到版本3的mapping model,以此完成数据迁移。

重命名实体或关系时,也是同样的步骤,设置目标实体/关系的Renaming ID字段。

3.设置options

修改实体后,还要在添加持久化存储时将options字典中自动迁移和自动推断的值设置为YES

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"ASDF.sqlite"];
NSError *error = nil;
// 数据库做轻量迁移时 传入此options字典
NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption: @YES,
NSInferMappingModelAutomaticallyOption: @YES};

if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:options error:&error]) { }
return _persistentStoreCoordinator;
}

这样,轻量级迁移就完成了,用户升级版本后启动时就不会因数据版本问题闪退了。


相关参考:

#©Fault
#©性能


数据库:Core Data
https://davidlii.cn/2017/12/02/coredata.html
作者
Davidli
发布于
2017年12月2日
许可协议