抽空学了段iOS开发,终于看到了数据持久化这一部分,这篇笔记记录下CoreData的基本用法。例子来自 https://www.raywenderlich.com/115695/getting-started-with-core-data-tutorial。
1 CoreData架构概述
CoreData是iOS提供的一套管理应用模型层(MVC中的Model)对象的框架,提供了模型对象的生命周期以及对象图形化管理和持久化等。图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所示:
再次启动,会发现我们表格并没有数据,实际上数据确实已经持久化了,需要我们读取出来。读取代码如下,加入到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所示:
另外说明的一点,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