<简书 — 刘小壮> http://www.jianshu.com/p/01f36026da7dgit
在以前的文章中,已经讲了不少关于
CoreData
使用相关的知识点。这篇文章中主要讲两个方面,NSFetchedResultsController
和版本迁移。github文章题目中虽然有**“高级”**两个字,其实讲的东西并不高级,只是由于上一篇文章中东西太多了,把两个较复杂的知识点挪到这篇文章中。数据库
文章中若有疏漏或错误,还请各位及时提出,谢谢!数组
在开发过程当中会常常用到UITableView
这样的视图类,这些视图类须要本身管理其数据源,包括网络获取、本地存储都须要写代码进行管理。缓存
而在CoreData
中提供了NSFetchedResultsController
类(fetched results controller
,也叫FRC
),FRC
能够管理UITableView
或UICollectionView
的数据源。这个数据源主要指本地持久化的数据,也能够用这个数据源配合着网络请求数据一块儿使用,主要看业务需求了。网络
本篇文章会使用UITableView
做为视图类,配合NSFetchedResultsController
进行后面的演示,UICollectionView
配合NSFetchedResultsController
的使用也是相似,这里就不都讲了。数据结构
就像上面说到的,NSFetchedResultsController
就像是上面两种视图的数据管理者同样。FRC
能够监听一个MOC
的改变,若是MOC
执行了托管对象的增删改操做,就会对本地持久化数据发生改变,FRC
就会回调对应的代理方法,回调方法的参数会包括执行操做的类型、操做的值、indexPath
等参数。多线程
实际使用时,经过FRC
**“绑定”一个MOC
,将UITableView
嵌入在FRC
的执行流程中。在任何地方对这个“绑定”**的MOC
存储区作修改,都会触发FRC
的回调方法,在FRC
的回调方法中嵌入UITableView
代码并作对应修改便可。app
由此能够看出FRC
最大优点就是,始终和本地持久化的数据保持统一。只要本地持久化的数据发生改变,就会触发FRC
的回调方法,从而在回调方法中更新上层数据源和UI
。这种方式讲的简单一点,就能够叫作数据带动UI。ide
可是须要注意一点,在FRC
的初始化中传入了一个MOC
参数,FRC
只能监测传入的MOC
发生的改变。假设其余MOC
对同一个存储区发生了改变,FRC
则不能监测到这个变化,不会作出任何反应。
因此使用FRC
时,须要注意FRC
只能对一个MOC
的变化作出反应,因此在CoreData
持久化层设计时,尽可能一个存储区只对应一个MOC
,或设置一个负责UI
的MOC
,这在后面多线程部分会详细讲解。
在写代码以前,先对以前的模型文件结构作一些修改。
讲FRC
的时候,只须要用到Employee
这一张表,其余表和设置直接忽略。须要在Employee
原有字段的基础上,增长一个String
类型的sectionName
字段,这个字段就是用来存储section title
的,在下面的文章中将会详细讲到。
下面例子是比较经常使用的FRC
初始化方式,初始化时指定的MOC
,还用以前讲过的MOC
初始化代码,UITableView
初始化代码这里也省略了,主要突出FRC
的初始化。
// 建立请求对象,并指明操做Employee表 NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"]; // 设置排序规则,指明根据height字段升序排序 NSSortDescriptor *heightSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES]; request.sortDescriptors = @[heightSort]; // 建立NSFetchedResultsController控制器实例,并绑定MOC NSError *error = nil; fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:context sectionNameKeyPath:@"sectionName" cacheName:nil]; // 设置代理,并遵照协议 fetchedResultController.delegate = self; // 执行获取请求,执行后FRC会从持久化存储区加载数据,其余地方能够经过FRC获取数据 [fetchedResultController performFetch:&error]; // 错误处理 if (error) { NSLog(@"NSFetchedResultsController init error : %@", error); } // 刷新UI [tableView reloadData];
在上面初始化FRC
时,传入的sectionNameKeyPath:
参数,是指明当前托管对象的哪一个属性当作section
的title
,在本文中就是Employee
表的sectionName
字段为section
的title
。从NSFetchedResultsSectionInfo
协议的indexTitle
属性获取这个值。
在sectionNameKeyPath:
设置属性名后,就以这个属性名做为分组title
,相同的title
会被分到一个section
中。
初始化FRC
时参数managedObjectContext:
传入了一个MOC
参数,FRC
只能监测这个传入的MOC
发生的本地持久化改变。就像上面介绍时说的,其余MOC
对同一个持久化存储区发生的改变,FRC
则不能监测到这个变化。
再日后面看到cacheName:
参数,这个参数我设置的是nil
。参数的做用是开启FRC
的缓存,对获取的数据进行缓存并指定一个名字。能够经过调用deleteCacheWithName:
方法手动删除缓存。
可是这个缓存并无必要,缓存是根据NSFetchRequest
对象来匹配的,若是当前获取的数据和以前缓存的相匹配则直接拿来用,可是在获取数据时每次获取的数据均可能不一样,缓存不能被命中则很难派上用场,并且缓存还占用着内存资源。
在FRC
初始化完成后,调用performFetch:
方法来同步获取持久化存储区数据,调用此方法后FRC
保存数据的属性才会有值。获取到数据后,调用tableView
的reloadData
方法,会回调tableView
的代理方法,能够在tableView
的代理方法中获取到FRC
的数据。调用performFetch:
方法第一次获取到数据并不会回调FRC
代理方法。
FRC
中包含UITableView
执行过程当中须要的相关数据,能够经过FRC
的sections
属性,获取一个遵照<NSFetchedResultsSectionInfo>
协议的对象数组,数组中的对象就表明一个section
。
在这个协议中有以下定义,能够看出这些属性和UITableView
的执行流程是紧密相关的。
@protocol NSFetchedResultsSectionInfo /* Name of the section */ @property (nonatomic, readonly) NSString *name; /* Title of the section (used when displaying the index) */ @property (nullable, nonatomic, readonly) NSString *indexTitle; /* Number of objects in section */ @property (nonatomic, readonly) NSUInteger numberOfObjects; /* Returns the array of objects in the section. */ @property (nullable, nonatomic, readonly) NSArray *objects; @end // NSFetchedResultsSectionInfo
在使用过程当中应该将FRC
和UITableView
相互嵌套,在FRC
的回调方法中嵌套UITableView
的视图改变逻辑,在UITableView
的回调中嵌套数据更新的逻辑。这样能够始终保证数据和UI的同步,在下面的示例代码中将会演示FRC
和UITableView
的相互嵌套。
// 经过FRC的sections数组属性,获取全部section的count值 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return fetchedResultController.sections.count; } // 经过当前section的下标从sections数组中取出对应的section对象,并从section对象中获取全部对象count - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return fetchedResultController.sections[section].numberOfObjects; } // FRC根据indexPath获取托管对象,并给cell赋值 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { Employee *emp = [fetchedResultController objectAtIndexPath:indexPath]; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifier" forIndexPath:indexPath]; cell.textLabel.text = emp.name; return cell; } // 建立FRC对象时,经过sectionNameKeyPath:传递进去的section title的属性名,在这里获取对应的属性值 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return fetchedResultController.sections[section].indexTitle; } // 是否能够编辑 - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } // 这里是简单模拟UI删除cell后,本地持久化区数据和UI同步的操做。在调用下面MOC保存上下文方法后,FRC会回调代理方法并更新UI - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // 删除托管对象 Employee *emp = [fetchedResultController objectAtIndexPath:indexPath]; [context deleteObject:emp]; // 保存上下文环境,并作错误处理 NSError *error = nil; if (![context save:&error]) { NSLog(@"tableView delete cell error : %@", error); } } }
上面是UITableView
的代理方法,代理方法中嵌套了FRC
的数据获取代码,这样在刷新视图时就能够保证使用最新的数据。而且在代码中简单实现了删除cell
后,经过MOC
调用删除操做,使本地持久化数据和UI
保持一致。
就像上面cellForRowAtIndexPath:
方法中使用的同样,FRC
提供了两个方法轻松转换indexPath
和NSManagedObject
的对象,在实际开发中这两个方法很是实用,这也是FRC
和UITableView
、UICollectionView
深度融合的表现。
- (id)objectAtIndexPath:(NSIndexPath *)indexPath; - (nullable NSIndexPath *)indexPathForObject:(id)object;
// Cell数据源发生改变会回调此方法,例如添加新的托管对象等 - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath { switch (type) { case NSFetchedResultsChangeInsert: [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeMove: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeUpdate: { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; Employee *emp = [fetchedResultController objectAtIndexPath:indexPath]; cell.textLabel.text = emp.name; } break; } } // Section数据源发生改变回调此方法,例如修改section title等。 - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch (type) { case NSFetchedResultsChangeInsert: [tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; default: break; } } // 本地数据源发生改变,将要开始回调FRC代理方法。 - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [tableView beginUpdates]; } // 本地数据源发生改变,FRC代理方法回调完成。 - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [tableView endUpdates]; } // 返回section的title,能够在这里对title作进一步处理。这里修改title后,对应section的indexTitle属性会被更新。 - (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName { return [NSString stringWithFormat:@"sectionName %@", sectionName]; }
上面就是当本地持久化数据发生改变后,被回调的FRC
代理方法的实现,能够在对应的实现中完成本身的代码逻辑。
在上面的章节中讲到删除cell
后,本地持久化数据同步的问题。在删除cell
后在tableView
代理方法的回调中,调用了MOC
的删除方法,使本地持久化存储和UI
保持同步,并回调到下面的FRC
代理方法中,在代理方法中对UI
作删除操做,这样一套由UI的改变引起的删除流程就完成了。
目前为止已经实现了数据和UI
的双向同步,即UI
发生改变后本地存储发生改变,本地存储发生改变后UI
也随之改变。能够经过下面添加数据的代码来测试一下,NSFetchedResultsController
就讲到这里了。
- (void)addMoreData { Employee *employee = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:context]; employee.name = [NSString stringWithFormat:@"lxz 15"]; employee.height = @(15); employee.brithday = [NSDate date]; employee.sectionName = [NSString stringWithFormat:@"3"]; NSError *error = nil; if (![context save:&error]) { NSLog(@"MOC save error : %@", error); } }
CoreData
版本迁移的方式有不少,通常都是先在Xcode
中,原有模型文件的基础上,建立一个新版本的模型文件,而后在此基础上作不一样方式的版本迁移。
本章节将会讲三种不一样的版本迁移方案,但都不会讲太深,都是从使用的角度讲起,能够知足大多数版本迁移的需求。
在已经运行程序并经过模型文件生成数据库后,再对模型文件进行的修改,若是只是修改已有实体属性的默认值、最大最小值、Fetch Request
等属性自身包含的参数时,并不会发生错误。若是修改模型文件的结构,或修改属性名、实体名等,形成模型文件的结构发生改变,这样再次运行程序就会致使崩溃。
在开发测试过程当中,能够直接将原有程序卸载就能够解决这个问题,可是本地以前存储的数据也会消失。若是是线上程序,就涉及到版本迁移的问题,不然会致使崩溃,并提示以下错误:
CoreData: error: Illegal attempt to save to a file that was never opened. "This NSPersistentStoreCoordinator has no persistent stores (unknown). It cannot perform a save operation.". No last error recorded.
然而在需求不断变化的过程当中,后续版本确定会对原有的模型文件进行修改,这时就须要用到版本迁移的技术,下面开始讲版本迁移的方案。
本文中讲的几种版本迁移方案,在迁移以前都须要对原有的模型文件建立新版本。
选中须要作迁移的模型文件 -> 点击菜单栏Editor -> Add Model Version -> 选择基于哪一个版本的模型文件(通常都是选择目前最新的版本),新建模型文件完成。
对于新版本模型文件的命名,我在建立新版本模型文件时,通常会拿当前工程版本号当作后缀,这样在模型文件版本比较多的时候,就能够很容易将模型文件版本和工程版本对应起来。
添加完成后,会发现以前的模型文件会变成一个文件夹,里面包含着多个模型文件。
在新建的模型文件中,里面的文件结构和以前的文件结构相同。后续的修改都应该在新的模型文件上,以前的模型文件不要再动了,在修改完模型文件后,记得更新对应的模型类文件。
基于新的模型文件,对Employee
实体作以下修改,下面的版本迁移也以此为例。
添加一个String
类型的属性,设置属性名为sectionName
。
此时还应该选中模型文件,设置当前模型文件的版本。这里选择将最新版本设置为刚才新建的1.1.0版本
,模型文件设置工做完成。
Show The File Inspector -> Model Version -> Current 设置为最新版本。
对模型文件的设置已经完成了,接下来系统还要知道咱们想要怎样迁移数据。在迁移过程当中可能会存在多种可能,苹果将这个灵活性留给了咱们完成。剩下要作的就是编写迁移方案以及细节的代码。
轻量级版本迁移方案很是简单,大多数迁移工做都是由系统完成的,只须要告诉系统迁移方式便可。在持久化存储协调器(PSC
)初始化对应的持久化存储(NSPersistentStore
)对象时,设置options
参数便可,参数是一个字典。PSC
会根据传入的字典,自动推断版本迁移的过程。
######字典中设置的key:
NSMigratePersistentStoresAutomaticallyOption
设置为YES
,CoreData
会试着把低版本的持久化存储区迁移到最新版本的模型文件。
NSInferMappingModelAutomaticallyOption
设置为YES
,CoreData
会试着以最为合理地方式自动推断出源模型文件的实体中,某个属性到底对应于目标模型文件实体中的哪个属性。
版本迁移的设置是在建立MOC
时给PSC
设置的,为了使代码更直观,下面只给出发生变化部分的代码,其余MOC
的初始化代码都不变。
// 设置版本迁移方案 NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES, NSInferMappingModelAutomaticallyOption : @YES}; // 建立持久化存储协调器,并将迁移方案的字典当作参数传入 [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:options error:nil];
假设须要对已存在实体进行更名操做,须要将重命名后的实体Renaming ID
,设置为以前的实体名。下面是Employee
实体进行操做。
修改后再使用实体时,应该将实体名设为最新的实体名,这里也就是Employee2
,并且数据库中的数据也会迁移到Employee2
表中。
Employee2 *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee2" inManagedObjectContext:context]; emp.name = @"lxz"; emp.brithday = [NSDate date]; emp.height = @1.9; [context save:nil];
轻量级迁移方案只是针对增长和改变实体、属性这样的一些简单操做,假设有更复杂的迁移需求,就应该使用Xcode
提供的迁移模板(Mapping Model
)。经过Xcode
建立一个后缀为.xcmappingmodel
的文件,这个文件是专门用来进行数据迁移用的,一些变化关系也会体如今模板中,看起来很是直观。
这里还以上面更改实体名,并迁移实体数据为例子,将Employee
实体迁移到Employee2
中。首先将Employee
实体更名为Employee2
,而后建立Mapping Model
文件。
Command + N 新建文件 -> 选择 Mapping Model -> 选择源文件 Source Model -> 选择目标文件 Target Model -> 命名 Mapping Model 文件名 -> Create 建立完成。
如今就建立好一个Mapping Model
文件,文件中显示了实体、属性、Relationships
,源文件和目标文件之间的关系。实体命名是EntityToEntity
的方式命名的,实体包含的属性和关联关系,都会被添加到迁移方案中(Entity Mapping
,Attribute Mapping
,Relationship Mapping
)。
在迁移文件的下方是源文件和目标文件的关系。
在上面图中更名后的Employee2
实体并无迁移关系,因为是更名后的实体,系统还不知道实体应该怎样作迁移。因此选中Mapping Model
文件的Employee2 Mappings
,能够看到右侧边栏的Source
为invalid value
。由于要从Employee
实体迁移数据过来,因此将其选择为Employee
,迁移关系就设置完成了。
设置完成后,还应该将以前EmployeeToEmployee
的Mappings
删除,由于这个实体已经被Employee2
替代,它的Mappings
也被Employee2 Mappings
所替代,不然会报错。
在实体的迁移过程当中,还能够经过设置Predicate
的方式,来简单的控制迁移过程。例如只须要迁移一部分指定的数据,就能够经过Predicate
来指定。能够直接在右侧Filter Predicate
的位置设置过滤条件,格式是$source.height < 100
,$source
表明数据源的实体。
若是还存在更复杂的迁移需求,并且上面的迁移方式不能知足,能够考虑更复杂的迁移方式。假设要在迁移过程当中,对迁移的数据进行更改,这时候上面的迁移方案就不能知足需求了。
对于上面提到的问题,在Mapping Model
文件中选中实体,能够看到Custom Policy
这个选项,选项对应的是NSEntityMigrationPolicy
的子类,能够建立并设置一个子类,并重写这个类的方法来控制迁移过程。
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error;
版本迁移在需求的变动中确定是要发生的,可是咱们应该尽可能避免这样的状况发生。在最开始设计模型文件数据结构的时候,就应该设计一个比较完善而且容易应对变化的结构,这样后面就算发生变化也不会对结构主体形成大的改动。
好多同窗都问我有Demo
没有,其实文章中贴出的代码组合起来就是个Demo
。后来想了想,仍是给本系列文章配了一个简单的Demo
,方便你们运行调试,后续会给全部博客的文章都加上Demo
。
Demo
只是来辅助读者更好的理解文章中的内容,应该博客结合Demo
一块儿学习,只看Demo
仍是不能理解更深层的原理。Demo
中几乎每一行代码都会有注释,各位能够打断点跟着Demo
执行流程走一遍,看看各个阶段变量的值。
Demo地址:刘小壮的Github
这两天更新了一下文章,将CoreData
系列的六篇文章整合在一块儿,作了一个PDF
版的《CoreData Book》,放在我Github上了。PDF
上有文章目录,方便阅读。
若是你以为不错,请把PDF帮忙转到其余群里,或者你的朋友,让更多的人了解CoreData,衷心感谢!