IOS文章收集OC语言特性iOS Developer

「死磕」Core Data——入门

2017-04-15  本文已影响839人  AntonyWong

适读对象:

Core Data,iOS中一种保存和读取数据的机制。以学习曲线陡峭而闻名~

因为我是文科狗转行的程序猿,并没有学过数据库相关课程,也欣赏不出SQLite的美,所以之前的项目一直用NSKeyedArchiverNSKeyedUnarchiver(固化)进行数据的本地保存(所幸我接触的项目,数据都不会太复杂)。

其实一开始接触iOS开发,就有阅读过Core Data相关内容。不过一来当时水平太低,看不太懂;二来Core Data本来也难学;三来经手的项目也没有强制使用Core Data;四来国内使用Core Data的开发者也不占主流。所以花了很长很长一段时间才入了门。过程算是曲折,所以标题哗众取宠地用了「死磕」二字。

「太长不看版」

本文确实比较长(从侧面印证Core Data内容确实多),所以这里写一个「太长不看版」,「以飨读者」:

Core Data使用流程:

备注:如果创建项目时勾选了「Use Core Data」,会自动帮你创建好上述这些内容。

OK,基本上就是这些东西了~

术语

CoreData学习曲线陡峭的原因之一,术语太多算一个。所以这里整理一下,如下:

iOS Core Data 示意图iOS Core Data 示意图

Core Data Stack

感觉理解起来有点抽象,先看官方定义:

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个框框。

Persistent Container

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 Object Context。

可以理解为是一块内存,提供了和Managed Objects交互的场所。也称为:The Context或者MOC。NSManagedObjectContext类实例。

备注:对数据进行删除、保存、查询,都要用到NSManagedObjectContext类的相关方法。

对应示意图第3个框框。

Managed Object Model

直观点,你可以把它理解为就是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个框框。

Managed Object。

就是需要保存的数据,是NSManagenObject类实例。(对应App中的「对象」)

就我的理解,Managed Object和上面提到的Entity,本质上是同一个东西,就是你的数据对象,只不过是在可视化操作和纯代码操作中的不同称谓。

对应示意图第5的那些框框。

Persistent Store Coordinator

协调Context和Persistent Store的一个角色。NSPersistentStoreCoordinator类实例。

如果只是对数据进行简单的增删查改,我们并不需要接触到这个类。

对应示意图第6个框框。

Persistent Store

可以理解为保存数据的地方。用于设置保存数据的方式、以及保存的路径等。(保存数据的方式指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」的初始化。

1、初始化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。

情况1:在既有项目添加Core Data功能(只需支持iOS10)

由于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)。

情况2:在既有项目初始化Core Data Stack(需兼容iOS8、9、10等系统)

因为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还是比较麻烦的。

当然,如果你不想做这个判断,只用上面方法初始化即可,这个方法在新旧系统都正常工作。

情况3:直接勾选Core Data

创建项目时,如果直接勾选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类似),这里不再赘述。

2、创建「managed object model」

好了,有了「工作台」,接着就需要「材料」了。也就是你要保存什么东西,这些东西有什么特性,这些东西之间有什么关系……Xcode提供了一套可视化的方案让我们「描述」这部分内容。

创建xcdatamodeld文件

快捷键: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类实例进行的(我们创建的实体,都是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子类,有如下两种办法

注意,第二种方式创建NSManagedObject子类,默认语言是Swift,如果需要改为OC,则到「File inspector」中修改,如下:

修改创建NSManagedObject子类的语言修改创建NSManagedObject子类的语言

3、增

好啦,有了「工作台」(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方法创建一个新的对象,会崩溃。

4、删

删除数据比较简单,直接调用NSManagedObjectContextdeleteObject:方法即可。当然,要怎么获取所要删除的对象,就自己斟酌了,可以通过NSFetchRequest查询获取要删除的对象,也可以用NSFetchedResultsController的objectAtIndexPath:方法拿到要删除的对象(NSFetchedResultsController另一篇文章再介绍)

- (void)removeUser:(SPKUser *)user {
    [_context deleteObject:user];
}

5、查(Fetching Objects)

查询功能,是被官方特别强调的一个功能,据闻可以玩出很多花样儿~

- (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;
}

上面是一个最简单的查询,调用NSManagedObjectContextexecuteFetchRequest: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

6、改

修改数据,和上面的增加一条数据的情况比较相似,直接对属性进行修改。先查询到你要的数据对象,再重新赋值即可。

如果要大批量修改数据,将数据从沙盒加载到内存,再进行修改,不利于性能,所以可以使用NSBatchUpdateRequestNSBatchDeleteRequest,进行批量的修改或者删除。这种方法直接在数据库内完成,无需加载到内存,利于性能提升。(但进行批处理后,因为操作是在数据库中完成的,要注意合并更新到Context中,以保持两者一致)

关于批处理,可以参考《New in Core Data and iOS 8: Batch Updating》,这里不再展开( 其实我自己暂时也没用过:D )

7、保存

保存比较简单,直接调用NSManagedObjectContext的save:方法即可,如下:

- (void)save {
    NSError *error = nil;
    if ([_context save:&error] == NO) {
        NSAssert(NO, @"Error saving context %@\n%@", [error localizedDescription], [error userInfo]);
    }
}

也可以调用NSManagedObjectContexthasChanges方法,来判断:在数据有变化的情况下再调用save:方法。

注意:在调用save方法之前,上面做的所有操作(增、删、改),都只是保存在内存中,并不会固化到沙盒中。

版本「迁移」

应用场景:修改了数据结构(比如说某个实体增加了一个特性),这时候就要进行版本迁移了,否则已经安装旧App的手机,在更新应用后,两边数据结构不一致导致不能识别,会崩溃。

步骤:

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
    [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

更详细的代码,上面用旧方法创建Core Data Stack时也有涉及。

大家也可以自己验证一下,不进行版本迁移,直接修改.xcdatamodeld文件,然后运行程序,会报什么错。

以上是自动、轻量化的版本迁移,至于更复杂的版本迁移,我目前也没有接触到,不再展开。可以参考:

自定义 Core Data 迁移

Core Data Model Versioning and Data Migration Programming Guide

End

认识CoreData-初识CoreData》系列文章,写得很详细,推荐阅读。

以上就是Core Data的入门用法(文科狗,不容易啊 XD )。

上一篇下一篇

猜你喜欢

热点阅读