适读对象:
Core Data,iOS中一种保存和读取数据的机制。以学习曲线陡峭而闻名~
因为我是文科狗转行的程序猿,并没有学过数据库相关课程,也欣赏不出SQLite的美,所以之前的项目一直用NSKeyedArchiver和NSKeyedUnarchiver(固化)进行数据的本地保存(所幸我接触的项目,数据都不会太复杂)。
其实一开始接触iOS开发,就有阅读过Core Data相关内容。不过一来当时水平太低,看不太懂;二来Core Data本来也难学;三来经手的项目也没有强制使用Core Data;四来国内使用Core Data的开发者也不占主流。所以花了很长很长一段时间才入了门。过程算是曲折,所以标题哗众取宠地用了「死磕」二字。
本文确实比较长(从侧面印证Core Data内容确实多),所以这里写一个「太长不看版」,「以飨读者」:
Core Data使用流程:
备注:如果创建项目时勾选了「Use Core Data」,会自动帮你创建好上述这些内容。
insertNewObjectForEntityForName:inManagedObjectContext:
,添加数据(对象)deleteObject:
方法删除数据executeFetchRequest:error:
方法,查询数据save:
方法保存数据。OK,基本上就是这些东西了~
CoreData学习曲线陡峭的原因之一,术语太多算一个。所以这里整理一下,如下:
iOS Core Data 示意图
感觉理解起来有点抽象,先看官方定义:
The Core Data stack is a collection of framework objects that are accessed as part of the initialization of Core Data and that mediate between the objects in your application and external data stores.
说是一个对象的集合,由4个主要对象构成:
我是这样理解的:Core Data Stack,就是进行数据增删查改、保存的「工作台」,Apple提供这样一个「工作台」,让你方便进行数据的保存。无需关心实现细节。
对应示意图第1个框框。
NSPersistentContainer是iOS 10、 macOS 10.12之后才出现的新类。引入这个新类的目的之一,就是为了简化创建Core Data Stack这个工作台的过程。所以,在iOS10之前,创建Core Data Stack会复杂一些。
而Persistent Container也有另一个新类NSPersistentStoreDescription,可以利用这个类,进行一些定制化设置,比如自定义存储路径、设置存储数据方式等(Core Data支持SQLite、XML、Binary、InMemory 4中方式存储数据)。
备注:iOS10中,如果利用NSPersistentContainer创建Core Data Stack,预设的是NSSQLiteStoreType类型。并且默认打开了自动轻量化版本迁移功能(换言之,在iOS10之前,需要手动进行相关设置,才能打开版本迁移功能)。
对应示意图第2个虚线框框。
可以理解为是一块内存,提供了和Managed Objects交互的场所。也称为:The Context或者MOC。NSManagedObjectContext类实例。
备注:对数据进行删除、保存、查询,都要用到NSManagedObjectContext类的相关方法。
对应示意图第3个框框。
直观点,你可以把它理解为就是Xcode中后缀为xcdatamodel的文件。在这个文件里,你可以通过非代码、可视化的方式,定义对象、对象的属性、对象之间的关系(Core Data把对象称呼为「实体」、对象的属性称呼为「特性」)。
Managed Object Model,就是Core Data中用于描述实体、实体特性、实体间关系的一套方案。
它是NSManagenObjectModel的类实例(也可以通过纯代码实现.xcdatamodel文件的内容)。也称为:The Model, Data Model, Schema或Object Graph。
换言之,Managed Object Model定义了你App的整个数据结构。
下面3个,是在设置.xcdatamodel文件时会遇到的3个术语。
NSEntityDescription类实例,用于定义一个对象。一个「实体」,最少要有「名字」和「类名」(如果没有设置类名,默认是NSManagedObject类)。
「实体特性」。NSAttributeDescription类实例。就是Entity的特性,对应App中的创建类时的属性。
「实体关系」。 NSRelationshipDescription类实例。用于描述Entity之间的关系。
对应示意图第4个框框。
就是需要保存的数据,是NSManagenObject类实例。(对应App中的「对象」)
就我的理解,Managed Object和上面提到的Entity,本质上是同一个东西,就是你的数据对象,只不过是在可视化操作和纯代码操作中的不同称谓。
对应示意图第5的那些框框。
协调Context和Persistent Store的一个角色。NSPersistentStoreCoordinator类实例。
如果只是对数据进行简单的增删查改,我们并不需要接触到这个类。
对应示意图第6个框框。
可以理解为保存数据的地方。用于设置保存数据的方式、以及保存的路径等。(保存数据的方式指SQLite、XML、Binary、InMemory4种)。NSPersistentStore类实例。也称为The Store或者Database。
在iOS10之前,如果需要支持版本迁移功能,需要在创建NSPersistentStore类实例时,传入相应的options参数。而在iOS10中,则会自动打开版本迁移功能,并默认设置数据类型为NSSQLiteStoreType(见上面的名词:「NSPersistentStoreDescription」)。
「版本迁移」,一开始对这个名字很是迷惑,还以为是将数据模型从一个App迁移到另外一个App。其实,是在内部进行「迁移」。
简单说,假如修改了数据模型(比如修改了. xcdatamodel文件:增加了实体,增加了特性等等),为了防止使用者在更新App后,由于数据模型不一致导致崩溃,需要进行一定的处理,这个处理,他们叫「版本迁移」(叫「版本升级」不是更合适吗~)。
对应示意图第7个框框。
可参考以下表格,对照进行理解(这个表格或许不慎严谨)
数据库术语 | 代码中的术语 | Core Data中的术语 |
---|---|---|
表格 | 类 | 实体 / Entity(NSEntityDescription类实例) |
列 | 属性 | 实体特性(Attribute) |
行 | 对象(类实例) | NSManagedObject(子)类实例 |
大部分教程是先创建「managed object model」,再初始化「Core Data Stack」的。因为我这里把「Core Data Stack」比喻成「工作台」,所以这篇文章先进行「Core Data Stack」的初始化。
上面我们将Core Data Stack比喻成一个「工作台」,是一切操作的所在地。
不过由于iOS10新引进了NSPersistentContainer类,然后新建项目又可以选择勾选Core Data与否。所以情况变得稍稍有点复杂。
这里分三种情况:1、在既有项目(只需支持iOS10)初始化Core Data Stack;2、在既有项目(需兼容iOS8、9、10等系统)初始化Core Data Stack;3、新建项目时直接勾选了Core Data。
由于iOS10引进了NSPersistentContainer,如果单单只支持iOS10系统,初始化Core Data Stack相比以前简单很多。
// 我们先声明了一个NSPersistentContainer类型的属性:persistentContainer,在适合的时间调用initWithName:对其初始化
// 这里的Name参数,需要和后续创建的.xcdatamodeld模型文件名称一致。
_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"MoveBand"];
// 调用loadPersistentStoresWithCompletionHandler:方法,完成Core Data Stack的最中初始化。
// 如果不能初始化成功,在Block回调中打印错误,方便调试
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription * _Nonnull description, NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Fail to load Core Data Stack : %@", error);
abort();
}
else {
...
}
}];
就两步:
更详细的说明,可参考官方文档Initializing the Core Data Stack
备注:你可以仿照Xcode所创建的模版,直接在AppDelegate类中桥敲以上代码。也可以新建一个专门负责储存功能的类,在这个类中敲这段代码。(我一般不喜欢将这部分代码放在AppDelegate类中,所以我创建工程的时候,都不会勾选Use Core Data)。
因为NSPersistentContainer不兼容iOS10之前的系统。所以,如果你已经用了NSPersistentContainer初始化了Core Data Stack,但同时也要兼容iOS8、9等系统,就需要在代码中检查,如果是旧的系统,就需要用旧的方法初始化Core Data Stack了。示例如下:
- (instancetype)init
{
self = [super init];
if (self) {
NSInteger majorVersion = [NSProcessInfo processInfo].operatingSystemVersion.majorVersion;
if (majorVersion < 10) {
// iOS10以下的系统, 用旧有的方法初始化Core Data Stack
[self initializeCoreDataLessThaniOS10];
}
else {
// iOS10的系统, 用新的方法(详见上面介绍的情况1)
[self initializeCoreData];
}
}
return self;
}
- (void)initializeCoreDataLessThaniOS10 {
// Get managed object model(拿到模型文件,也就是.xcdatamodeld文件(我们会在初始化完Core data Stack后创建))
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"MoveBand" withExtension:@"momd"];
NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSAssert(mom != nil, @"Error initalizing Managed Object Model");
// Create persistent store coordinator(创建NSPersistentStoreCoordinator对象(需要传入上述创建的NSManagedObjectModel对象))
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
// Creat managed object context(创建NSManagedObjectContext对象(_context是声明在.h文件的属性——因为其他类也要用到这个属性))
_context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
// assgin persistent store coordinator(赋值persistentStoreCoordinator)
_context.persistentStoreCoordinator = psc;
// Create .sqlite file(在沙盒中创建.sqlite文件)
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentsURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *storeURL = [documentsURL URLByAppendingPathComponent:@"DataModel.sqlite"];
// Create persistent store(异步创建NSPersistentStore并add到NSPersistentStoreCoordinator对象中,作用是设置保存的数据类型(NSSQLiteStoreType)、保存路径、是否支持版本迁移等)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 用于支持版本迁移的参数
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
NSError *error = nil;
NSPersistentStoreCoordinator *psc = _context.persistentStoreCoordinator;
// 备注,如果options参数传nil,表示不支持版本迁移
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:options
error:&error];
NSAssert(store != nil, @"Error initializing PSC: %@\n%@", [error localizedDescription], [error userInfo]);
});
}
可以看到,旧方法初始化Core Data Stack还是比较麻烦的。
当然,如果你不想做这个判断,只用上面方法初始化即可,这个方法在新旧系统都正常工作。
创建项目时,如果直接勾选Core Data复选框,项目模版会在AppDelegate类中直接帮你初始化好Core Data Stack,自动创建和上面情况1类似的代码(Xcode8)
在AppDelegate.h文件
#import <UIKit/UIKit.h>
// 导入了CoreData框架
#import <CoreData/CoreData.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
// 在.h文件声明一个NSPersistentContainer类型的属性(为了让其他类可以调用)
@property (readonly, strong) NSPersistentContainer *persistentContainer;
// 声明了一个保存数据的方法
- (void)saveContext;
@end
在AppDelegate.m文件
@implementation AppDelegate
……
#pragma mark - Core Data stack
@synthesize persistentContainer = _persistentContainer;
- (NSPersistentContainer *)persistentContainer {
@synchronized (self) {
if (_persistentContainer == nil) {
// 实例化NSPersistentContainer对象。
// 注意:参数传入的名称,就是.xcdatamodeld文件名称(两者需要一直)(勾选Core Data后,会自动创建一个.xcdatamodeld文件)
_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"CoreDataTestUseCoreData"];
// 加载persistent stores,实现最终的Core Data stack的创建
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
// 如果有错误,打印出来
if (error != nil) {
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}];
}
}
return _persistentContainer;
}
#pragma mark - Core Data Saving support
- (void)saveContext {
// 注意这句,NSManagedObjectContext对象,是通过上面创建的NSPersistentContainer对象的属性viewContext获取的,无需自己初始化(iOS10之前要自己初始化)
NSManagedObjectContext *context = self.persistentContainer.viewContext;
NSError *error = nil;
// 保存数据,直接用的是NSManagedObjectContext的save:方法,很简单。
if ([context hasChanges] && ![context save:&error]) {
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}
系统帮我们创建了一个NSPersistentContainer实例,以及一个saveContext方法。(并且已经帮我们创建了.xcdatamodeld模型文件)
注意看saveContext,我们通过NSPersistentContainer的属性viewContext拿到NSManagedObjectContext对象,再通过save:方法进行数据的保存。
因为系统并没有帮我们适配旧系统,所以如果App要在非iOS10的旧系统运行,还需要做类似情况2的工作。
如果是Xcode8之前的版本自动创建的Core Data Stack,会不一样(跟情况2类似),这里不再赘述。
好了,有了「工作台」,接着就需要「材料」了。也就是你要保存什么东西,这些东西有什么特性,这些东西之间有什么关系……Xcode提供了一套可视化的方案让我们「描述」这部分内容。
快捷键:Command + N,选择Core Data栏目下的「Data Model」,就可以创建一个.xcdatamodeld模型文件(managed object model),名字随意。接着我们就可以往里面添加材料了。
这部分用一张图概括:
添加实体、实体的特性、关系示意图
坑:这里有个坑,在Xcode8中,Codegen下拉选择框中增加了Class/Definition这一选项,而且是默认的预设值,这时候系统会自动帮我们这个实体创建了NSManagedObject子类,最坑的是,这些自动创建的类,在导航面板是看不见的!!!然后你很容易再重复手动创建NSManagedObject子类,这时候就会报类似「duplicate symbol _OBJC_METACLASS_Photography in:...」这类错误。
所以,如果你想自己手动创建NSManagedObject子类,就要把系统预设的Class/Definition改为Manual/None。
好了,通过上面的一步,我们知道我们要保存的是什么东西,以及知道他们是什么关系了(数据模型建好了)。
这时候其实可以进行数据的增删查改了。但是这时候赋值(或者修改)一条数据,都是通过NSManagedObject类实例进行的(我们创建的实体,都是NSManagedObject类型的),类似如下:
NSManagedObject *newUser = …… // 这里聚焦在数据的赋值与取值, 暂时省略插入一条数据的方法
// 赋值
[newUser setValue:@"Antony" forKey:@"name"];
[newUser setValue:@123 forKey:@"userID"];
// 取值
NSManagedObject *selectedUser = ……
NSString *name = [selectedUser valueForKey@"name"];
……
以上的存取值方式,有点类似字典。不直观,敲字符串也容易出错。所以,我们通常都会创建NSManagedObject的子类,用点语法直接进行存取操作。
在.h文件
#import <CoreData/CoreData.h>
@interface SPKUser : NSManagedObject
@property (copy, nonatomic) NSString *name;
@property (nonatomic) int64_t userID;
@end
在.m文件
#import "SPKUser.h"
@implementation SPKUser
// 在OC中,将某个属性实现为@dynamic,表示编译器在编译时不会对这个属性的存取方法(getter/setter)做检查(由程序员自己提供存取方法)。在Core Data中,由Core Data实现。
@dynamic name;
@dynamic userID;
@end
然后就可以这样:
- (void)addNewUser {
SPKUser *newUser = ……;
newUser.name = @"Antony";
newUser.userID = 123;
NSLog(@"添加了一个user");
}
所以,这就是应用NSManagedObject子类的好处。
创建NSManagedObject子类,有如下两种办法
注意,第二种方式创建NSManagedObject子类,默认语言是Swift,如果需要改为OC,则到「File inspector」中修改,如下:
修改创建NSManagedObject子类的语言
好啦,有了「工作台」(Core Data Stack),又有了「材料」(managed object model),可以撸起袖子干了……(第一张示意图,其实都有对增删查、保存方法有所提及)
- (void)addNewUser {
SPKUser *newUser = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:_context];
newUser.name = @"Antony";
newUser.userID = 123;
}
看以上代码,增加一条数据,并不是调用NSManagedObjectContext类中的某个方法,而是用了NSEntityDescription的类方法insertNewObjectForEntityForName:inManagedObjectContext:
,第一个参数传入实体名称,第二个参数传入context(因为Core Data是支持多个context的,所以这里传入context参数以界定是在哪个context中操作)。
该方法会返回一个NSManagedObject,或其子类的对象,然后就可以对该对象进行赋值操作了。
注意:此时数据只存在内存中,并没有固化、保存到沙盒。还需要通过特定的保存方法才能固化到沙盒。
另外,不能用alloc、init方法创建一个新的对象,会崩溃。
删除数据比较简单,直接调用NSManagedObjectContext的deleteObject:
方法即可。当然,要怎么获取所要删除的对象,就自己斟酌了,可以通过NSFetchRequest查询获取要删除的对象,也可以用NSFetchedResultsController的objectAtIndexPath:方法拿到要删除的对象(NSFetchedResultsController另一篇文章再介绍)
- (void)removeUser:(SPKUser *)user {
[_context deleteObject:user];
}
查询功能,是被官方特别强调的一个功能,据闻可以玩出很多花样儿~
- (NSArray *)allUsers {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
NSError *error = nil;
NSArray *results = [_context executeFetchRequest:request error:&error];
if (!results) {
NSLog(@"Error fetching Employee objects: %@\n%@", [error localizedDescription], [error userInfo]);
abort();
}
return results;
}
上面是一个最简单的查询,调用NSManagedObjectContext的 executeFetchRequest:error:
方法,传入一个NSFetchRequest对象作为参数,这个参数定义了要取回的是哪个实体。
另外,还可以通过NSPredicate(「谓语」,也有翻译为「断言」的)进行数据筛选,只获取某些符合条件的数据。还可以通过NSSortDescriptor设置获取数据的排列顺序。如下:
- (NSArray *)allUsers {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 只取回firstName是Antony的数据
NSString *firstName = @"Antony";
[request setPredicate:[NSPredicate predicateWithFormat:@"firstName == %@", firstName]];
// 取回的数据按userID进行由小到大(升序)的排序
NSSortDescriptor *userIDSort = [NSSortDescriptor sortDescriptorWithKey:@"userID" ascending:YES];
// 注意,这个参数是一个数组,所以排序可以有多个条件,比如先按身高从低到高排,满足此条件后再按照名字首字母A~Z从前到后排。这时候,身高的Sort Descriptor放在数组前面,名字的Sort Descriptor放在数组后面。
[request setSortDescriptors:@[userIDSort]];
NSError *error = nil;
NSArray *results = [_context executeFetchRequest:request error:&error];
if (!results) {
NSLog(@"Error fetching Employee objects: %@\n%@", [error localizedDescription], [error userInfo]);
abort();
}
return results;
}
关于NSPredicate更详细的用法,可参考官方文档:Predicate Programming Guide
修改数据,和上面的增加一条数据的情况比较相似,直接对属性进行修改。先查询到你要的数据对象,再重新赋值即可。
如果要大批量修改数据,将数据从沙盒加载到内存,再进行修改,不利于性能,所以可以使用NSBatchUpdateRequest 、NSBatchDeleteRequest,进行批量的修改或者删除。这种方法直接在数据库内完成,无需加载到内存,利于性能提升。(但进行批处理后,因为操作是在数据库中完成的,要注意合并更新到Context中,以保持两者一致)
关于批处理,可以参考《New in Core Data and iOS 8: Batch Updating》,这里不再展开( 其实我自己暂时也没用过:D )
保存比较简单,直接调用NSManagedObjectContext的save:
方法即可,如下:
- (void)save {
NSError *error = nil;
if ([_context save:&error] == NO) {
NSAssert(NO, @"Error saving context %@\n%@", [error localizedDescription], [error userInfo]);
}
}
也可以调用NSManagedObjectContext的hasChanges
方法,来判断:在数据有变化的情况下再调用save:
方法。
注意:在调用save方法之前,上面做的所有操作(增、删、改),都只是保存在内存中,并不会固化到沙盒中。
应用场景:修改了数据结构(比如说某个实体增加了一个特性),这时候就要进行版本迁移了,否则已经安装旧App的手机,在更新应用后,两边数据结构不一致导致不能识别,会崩溃。
步骤:
切换到新版的.xcdatamodeld文件
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
更详细的代码,上面用旧方法创建Core Data Stack时也有涉及。
大家也可以自己验证一下,不进行版本迁移,直接修改.xcdatamodeld文件,然后运行程序,会报什么错。
以上是自动、轻量化的版本迁移,至于更复杂的版本迁移,我目前也没有接触到,不再展开。可以参考:
Core Data Model Versioning and Data Migration Programming Guide
《认识CoreData-初识CoreData》系列文章,写得很详细,推荐阅读。
以上就是Core Data的入门用法(文科狗,不容易啊 XD )。