iOS学习笔记(10)-CoreData初步

抽空学了段iOS开发,终于看到了数据持久化这一部分,这篇笔记记录下CoreData的基本用法。例子来自 https://www.raywenderlich.com/115695/getting-started-with-core-data-tutorial。

1 CoreData架构概述

CoreData是iOS提供的一套管理应用模型层(MVC中的Model)对象的框架,提供了模型对象的生命周期以及对象图形化管理和持久化等。图1是CoreData的架构图。

iOS学习笔记(10)-CoreData初步_第1张图片
图1 CoreData架构图

从图中可以看到CoreData涉及这么几个对象(这部分可以先跳过,看完后面的示例再回头来对照一下):

  • NSManagedObject
    可以简单理解为数据库表中的记录。数据库表的元数据则是存储在NSEntityDescriptor这个对象中。

  • NSManagedObjectContext(以下简写为Context)

    • NSManagedObject的对象池,负责NSManagedObject的生命周期的管理,包括从Persistent Store查询数据以及持久化存储到Persistent Store中。一个NSManagedObject都关联着一个Context对象,它的managedObjectContext属性就是对应这个Context对象
    • 一个Context对应一个队列。 一般一个应用只有一个NSManagedObjectContext,少数应用会用到多个。有一点要注意,Context对象不是线程安全的,多线程操作CoreData时一般是用多个Context对象,不同的线程采用不同的Context对象,可以保证线程安全。
  • NSPersistentStoreCoordinator
    -是NSManagedObjectModel和NSPersistentStore的协调器。NSManagedObjectModel定义了数据结构,而NSPersistentStoreCoordinator则是从NSPersistentStore中取数据并转换为NSManagedObjectModel的对象,然后传递给Context。

    • 需要注意的是,虽然Context和NSPersistentStoreCoordinator都不是线程安全的,但是多个不同的Context对象可以用同一个NSPersistentStoreCoordinator,因为不同的Context对象在使用NSPersistentStoreCoordinator的时候,Context会正确的加锁来保证多线程的持久化的顺序
  • NSManagedObjectModel

    • 用于描述数据结构。在初始化CoreData Stack的时候,NSManagedObjectModel会加载到内存中。在它初始化完毕后,我们就可以构建NSPersistentStoreCoordinator了。
  • NSPersistentStore

    • CoreData的持久化存储默认用的存储方式为NSQLiteStoreType,也就是底层用的SQLite做持久化存储。
    • 除了SQLite这种非原子型的,还有NSBinaryStoreType和NSInMemoryStoreType这两种原子型的,当然NSInMemoryStoreType是存储在内存中,严格来说不算持久化存储,它常用来单元测试和缓存。原子型的存储会一次加载所有的对象到内存中,后续的操作除了save外不会再操作磁盘,效率较高。

2 实例

这次要实现的功能很简单,就是在app中添加一个Table View,然后点击一个添加条目的按钮,输入一个人名添加到表格中,同时要持久化到数据库中。

新建一个Single View Application工程,记得勾选Use Core Data。创建完成后,可以发现与不使用Core Data的不同之处在于工程目录下多了个后缀为xcdatamodeld的文件,这个文件就是用来设计数据模型的,xcode提供了一个可视化的工具来方便添加数据模型。此外AppDelegate头文件和实现文件里面多了些Core Data的属性声明和属性设置代码,如下所示:

//AppDelegate.h里面的属性
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) 

//AppDelegate.m里面的属性设置代码
NSPersistentStoreCoordinator *persistentStoreCoordinator;
- (NSManagedObjectModel *)managedObjectModel {
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    //初始化对象模型,扩展名是momd。
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreDataTutorial" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    // 创建Coordinator和Store,存储类型是SQLite,
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }

    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataTutorial.sqlite"];
    NSError *error = nil;
    NSString *failureReason = @"There was an error creating or loading the application's saved data.";
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        // 捕获错误并打印日志
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        dict[NSLocalizedDescriptionKey] = @"Failed to initialize the application's saved data";
        dict[NSLocalizedFailureReasonErrorKey] = failureReason;
        dict[NSUnderlyingErrorKey] = error;
        error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    
    return _persistentStoreCoordinator;
}

- (NSManagedObjectContext *)managedObjectContext {
    // 创建Context对象,注意用的队列是NSMainQueueConcurrencyType,即主队列。
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }
    
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        return nil;
    }
    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    return _managedObjectContext;
}

- (void)saveContext {
    //保存Context管理的数据到数据库中
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        NSError *error = nil;
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

添加一个Table View到视图中,然后在导航栏添加一个+按钮用于添加名字。点击按钮,弹出框可以输入要添加的名字,点击Save,则调用saveName方法保存数据到数据库中并更新表格数据。ViewController中代码如下:

- (IBAction)addName:(id)sender {
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"New Name" message:@"Add a new name" preferredStyle:UIAlertControllerStyleAlert];
    
    UIAlertAction *saveAction = [UIAlertAction actionWithTitle:@"Save" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        UITextField *textField = alert.textFields.firstObject;
        [self saveName:textField.text];
        [self.tableView reloadData];
    }];
    
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    }];
    
    [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
    }];
    
    [alert addAction:saveAction];
    [alert addAction:cancelAction];
    [self presentViewController:alert animated:YES completion:^{
    }];
}

- (void)saveName:(NSString *)name {
    //可以看到 不管是NSEntityDescription还是NSManagedObject,都要与一个Context关联。
    AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    NSManagedObjectContext *managedContext = appDelegate.managedObjectContext;
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:managedContext];
    NSManagedObject *person = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:managedContext];
    
    //设置值,并调用saveContext方法保存。    
    [person setValue:name forKey:@"name"];
    [appDelegate saveContext];
    [self.people addObject:person];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.people count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"];
    NSManagedObject *person = self.people[indexPath.row];
    //取值用valueForKey
    cell.textLabel.text = [person valueForKey:@"name"];
    return cell;
}

运行代码,可以发现效果如图2所示:

iOS学习笔记(10)-CoreData初步_第2张图片
图2 运行效果图

再次启动,会发现我们表格并没有数据,实际上数据确实已经持久化了,需要我们读取出来。读取代码如下,加入到viewDidLoad中,这样表格中就会有数据了。

- (void)loadData {
    AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    NSManagedObjectContext *managedContext = appDelegate.managedObjectContext;
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
    
    NSError *error;
    NSArray *results = [managedContext executeFetchRequest:
       fetchRequest error:&error];
    if (error) {
        NSLog(@"fetch error, %@, info:%@", error, error.userInfo);
        return;
    }
    self.people = [results mutableCopy];
}

至此,简单的读写已经完成,下一节说下多线程CoreData问题。

3 多线程CoreData

有时候,比如应用要从数据库加载许多数据,那么如果还是放在主队列里面的话,会阻塞UI的加载和显示,APP也无法响应事件请求,因为UIKit的操作都是在主线程中完成的,这里就要用到了多线程。

在CoreData里面,要用多线程不需要dispatch_async来实现,我们可以在创建Context的时候指定队列为非主队列,然后用这个Context的performBlock方法执行即可,这样block里面的代码就是在另外一个队列(也是另外一个线程)中运行了。不管这个里面加载数据库的数据要多久,并不会阻塞UI的加载和事件响应,可以提升用户体验。

在代码中,创建了一个新的Context,指定的队列是NSPrivateQueueConcurrencyType,然后调用该context的performBlock,这样block中的代码是在另外的一个线程执行的,即便耗时很长,也不影响主线程。App的UI还是可以显示并接受事件响应的,即便数据加载没有执行完成。注意初始化privateContext的时候,指定了parentContext为managedContext,这是ios提供的一种新的操作方式,子Context操作数据的时候,会推送给它的parentContext即managedContext,managedContext通过persistentStoreCoordinator完成数据存储和查询,也就是说子Context发生的数据操作,父Context可以及时感知。同时,也可以保证多线程CoreData操作的安全性。当然,平级的子Context之间是无法感知的。架构如图3所示:

iOS学习笔记(10)-CoreData初步_第3张图片
图3 父子Context架构

另外说明的一点,Table View更新数据需要到主线程中执行,因为UIKit都是在主线程中执行的,如果不放在主线程中,会执行失败,数据不会刷新。

另外,关于Core Data并发操作,还有些疑惑的地方,后面理清了再补充,当然如果只是简单这样用是没有什么问题了的。

- (NSManagedObjectContext *)privateContext {
    if (!_privateContext) {
        AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
        NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
        
        _privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        _privateContext.parentContext = managedObjectContext;
    }
    return _privateContext;
  }

- (void)loadDataInPrivateContext {
    NSLog(@"pid:%d, tid:%@", getpid(), [NSThread currentThread]);
    // pid:4391, tid:{number = 1, name = main} 
    [self.privateContext performBlock:^{
        NSLog(@"pid in private:%d, tid:%@", getpid(), [NSThread currentThread]);
        //pid in private:4391, tid:{number = 3, name = (null)}
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Person"];
        NSArray *results = [self.privateContext executeFetchRequest:fetchRequest error:nil];
        self.people = [results mutableCopy];
        sleep(10);
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"pid in main queue:%d, tid:%@", getpid(), [NSThread currentThread]);
            //in main queue:4391, tid:{number = 1, name = main}
            [self.tableView reloadData];
        });
    }];
}

完整代码地址:
https://github.com/shishujuan/ios_study/tree/master/coredata/CoreDataTutorial

4 参考资料

  • Core Data from Scratch: Concurrency
  • Core Data Programming Guide
  • getting-started-with-core-data-tutorial

你可能感兴趣的:(iOS学习笔记(10)-CoreData初步)