iOS数据存储(下)

用FarBox写博客,发现了几个问题,文章的链接无法跟着Title一起变化,另外文章的顺序也无法调整,
但其跨平台编辑查看这点还是不错的,而且界面很简洁。

今天来聊一下数据存储可能用到的CoreData,CoreData在2014年的时候我也做过尝试,当时第一次尝试也遇到过一些坑,后来又专门抽时间整理了一些内容,其实学习并梳理Demo的时间过去有一段时间了,怕有些需要注意的点可能慢慢地淡忘了,今天还是先整理出来,后面有时间再慢慢完善。

其实我之前研究OpenCV的时候也过很多Demo,但终究觉得还是不另人满意,就没有整理出文章了,前段时间终于抽时间阅读了JSPatch的源码,但学习的深度还不够,只能说再慢慢努力了。

最后来聊一聊CoreData与iCloud相关的内容吧,说实话这两块一直争议不断,我之前有使用过CoreData,感觉使用起来还是顺手的,但iCloud在项目中一直都未曾使用过,因为我们自己有去服务器,而且还要跨平台,所以iCloud就一直没有使用过了,不过只focus iOS的同学还是值得使用。

在了解CoreData之前,建议大家先看一下Core Data概述,先对其有一个大致的了解。

其实CoreData是一个基于模型层的技术,对于模型之间的关系管理是它一个比较强大的地方,因此我们不能简单地把它看成是SQLite的升级,或者ORM,在细入了解CoreData之前,我们需要知道它的一个整体架构,来看一下下面这张图吧。
Image

从上面这张图可以看出来,NSPersistentStoreCoordinator在其中扮演了一个非常重要的作用,起到一个承上启下的作用,把Model与File关联起来。值得注意的是我们通常只有一个NSManagedObjectContext。

于是我们要使用CoreData的话,我们需要创建一个NSManagedObjectContext对象,以及为NSManagedObjectContext的persistentStoreCoordinator属性(即NSPersistentStoreCoordinator)。如果你在创建工程的时候直接勾选Core Data的话,这些都是直接生成的,但放在AppDelegate可能并不是你最好的选择。

我创建了一个单例,用于管理CoreData其创建的代码大致如下

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                         @YES, NSMigratePersistentStoresAutomaticallyOption,
                         @YES, NSInferMappingModelAutomaticallyOption,
                         @"MyAppCloudStore",NSPersistentStoreUbiquitousContentNameKey,nil];
self.model = [[NSManagedObjectModel alloc] initWithContentsOfURL:[self modelURL]];
self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.context.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model];
NSError *error;
[self.context.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self storeURL] options:options error:&error];
if (error) {
    NSLog(@"error = %@",error);
}
self.context.undoManager = [NSUndoManager new];

其中context为NSManagedObjectContext对象,其中model是我们自定义数据模型,因此,我们还需要提供Model的存放位置以及Core Data的存放位置。

- (NSURL*)storeURL
{
    NSURL* documentsDirectory = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
    return [documentsDirectory URLByAppendingPathComponent:@"usersummary.sqlite"];
}

- (NSURL*)modelURL
{
    return [[NSBundle mainBundle] URLForResource:@"UserSummaryModel" withExtension:@"momd"];
}

需要注意的几点,NSMigratePersistentStoresAutomaticallyOptionNSInferMappingModelAutomaticallyOption这两个参数构成了CoreData的自动化数据迁移,数据迁移的重要性我在iOS数据存储(中)中有阐述,除此之外还有两种数据的手法,之后再谈。NSPersistentStoreUbiquitousContentNameKey则是为iCloud准备的。

而对Core Data的读写操作可以通过如下方法来实现

- (UserSummaryModel *)insertUserSummary
{
    UserSummaryModel *model = [NSEntityDescription insertNewObjectForEntityForName:[UserSummaryModel entityName] inManagedObjectContext:self.context];
    return model;
}

- (UserSummaryModel *)fetchUserSummary
{
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:[UserSummaryModel entityName]];

//    NSAsynchronousFetchRequest *request = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult * _Nonnull result) {
//        result.progress.completedUnitCount/result.progress.totalUnitCount;
//        if (result.finalResult) {
//            
//        }
//    }];
//    
//    [self.context performBlock:^{
//        [self.context executeRequest:request error:nil];
//    }];

    NSError *error = NULL;
    NSArray *array = [self.context executeFetchRequest:fetchRequest error:&error];
    if (error) {
        NSLog(@"Error : %@\n", [error localizedDescription]);
    }

    return [array firstObject];
}

其中注释掉的部分为iOS8引入的异步读取数据的代码,读取数据的时候可以使用NSPredicate,做各种过滤筛选。[UserSummaryModel entityName]可以理解为SQLite里的Table。

当然最后,不要忘记保存数据

- (void)saveContext
{
    NSError *error = NULL;
    if (self.context && [self.context hasChanges] && ![self.context save:&error]) {
        NSLog(@"Error %@, %@", error, [error localizedDescription]);
        abort();
    }
}

接下来聊一聊天Core Data里面需要注意的地方以及一些非常特别的地方。


数据迁移

  • 轻量迁移方法1 在创建NSPersistentStoreCoordinator的时候使用下面两个参数 NSMigratePersistentStoresAutomaticallyOptionNSInferMappingModelAutomaticallyOption,这样Core Data就会自动进行数据迁移。

官方文档支持的轻量级迁移操作如下:

  • 为Entity简单的添加一个属性
  • 为Entity移除一个属性
  • 属性值由 Optional<-> Non-optional 之间转换
  • 为属性设置 Default Value
  • 重命名Entity或者Attribute
  • 增加一个新的relationship 或者删除一个已经存在的 relationship
  • 重命名relationship
  • 改变relationship to-one<-> to-many 等
  • 增加,删除Entities
  • 增加新的 Parent 或者 Child Entity
  • 从Hierarchy中移除Entities

  • 轻量迁移方法2
    先在Xcode中选中xcdatamodeld文件,再点击导航栏上的Editor,点击Add Model Version...,然后在原来的xcdatamodeld文件左边就会出现一个箭头,点击后里面会展开出多个文件,点击新的文件,修改你需要的内容,然后把Current Version设置为最新的版本如下图。
    Image

  • 自定义数据迁移
    如果上面两种方法还无法满足你的需求,可以考虑使用Mapping Model,先得把NSInferMappingModelAutomaticallyOption设置为NO。然后先进行迁移2的方法进行操作一遍,剩下的工作就可以参考文章中所写的,利用NSEntityMigrationPolicy或者渐进式迁移,从作者的Demo可以看出,数据迁移的成本是非常高的。


Fetch

上面简单地介绍了Core Data相关Fetch的内容,但事实上他可以做的事情还有很多,同时也存在诸多不足之处,需要大家多多理解才可以。
NSFetchRequest有一个predicate可以做数据过滤,设置request.returnsObjectsAsFaults = NO;可以直接把数据填充到返回的对象里面,这样可以提高性能,但也会增加内存的开销。

例如一个过滤条件可以写成如下样子,这里是在查询附近相关的数据

request.predicate = [NSPredicate predicateWithFormat:
                  @"(%@ <= longitude) AND (longitude <= %@)"
                  @"AND (%@ <= latitude) AND (latitude <= %@)",
                  @(minLongitude), @(maxLongitude), @(minLatitude), @(maxLatitude)];

除了使用AND这样的关键字之外,这儿有一个详细的Predicate Format String Syntax。值得注意的是,他的语法比SQLite还要少。

除此之外,我们也可以使用组合查找NSCompoundPredicate,其提供了并、或、非三个初始化方法。

另外一个值得一提的是,值到iOS8苹果才更新了Batch Updates技术,这样我们就像SQLite里面一样使用非常简单的语法业全局更新所有的数据了, 在iOS8还有一个新的技术Asynchronous Fetching,其可以与NSProgress直接结合使用,这样就可以做到异步进度更新了
后来iOS9又更新了Batch Deletions,可以批量删除数据。

如果要像SQLite里面一样使用聚合操作,比如 count(),sum(),max(),min(),avg() 等,则需要使用到propertiesToGroupBy这个属性,结合上NSExpressionDescriptionNSExpression就可以了。由此可以看出来Core Data在处理这些问题时比SQLite语法要更加复杂。


RelationShips

例如我们定义了如下数据模型

@interface Item : NSManagedObject
@property (nonatomic, retain) NSString* title;
@property (nonatomic, assign) NSNumber* order;
@property (nonatomic, retain) Item* parent;
@property (nonatomic, retain) NSSet* children;

则我们定义parent与children两个relationShip,但这里要注意的是不支持NSArray这种类型,必须用NSSet,同时如果要对数据进行排序的话,需要额外增加字段进行排序。
Image


NSFetchedResultsController

NSFetchedResultsController是作用在Core Data上的,通过NSFetchRequest来查询Core Data里面的数据.可以返回按照组分好的数据.这样便于UITableView来显示。我们可以通过如下方式来生成一个NSFetchedResultsController

- (NSFetchedResultsController*)childrenFetchedResultsController
{
    NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:[self.class entityName]];
    request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", self];
    request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES]];
    return [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
}

但Modle改变的时候NSFetchedResultsController能及时的发出通知。准确的说,应该是当NSManagedObjectContext发生改变的时候,NSFetchedResultsController能知道这些变化,然后发出通知出来,以便UITableview能及时的更新。
我们需要做的是实现NSFetchedResultsControllerDelegate,里面有三个比较重要的方法

- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller;

- (void)controllerDidChangeContent:(NSFetchedResultsController*)controller;

- (void)controller:(NSFetchedResultsController*)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath*)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath*)newIndexPath;

所以这个东西感觉天生就是为了UITableView或者UICollectionView而设计的,但这里也有坑,我在写Demo的时候遇到Delete某个数据的时候,需要Save后,才能刷新出来。


UndoManager

iOS中默认不使用UndoManager,需要主动生成,其可以撤销多个最近的数据库操作,与SQLite里面的rollback不是同一个概念,后者只能在内存中回滚。

self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];

这里要注意的是,开启这个模式后,性能上会有所损耗,所以如果没有需要的不要打开这个功能,如果对某些操作进行Undo操作,需要在前面加上[self.undoManager setActionName:actionName];来进行标记,这样方便后续进行Undo操作。


多线程安全

只要涉及到数据的存储,必然就绕不开处理时I/O的性能问题,为了避免阻塞主线程,我们通常想到的就是利用多线程,但是如果使用多线程处理的话,就要解决线程修改的数据同步到主线程,便于UI的刷新。

官方推荐的方式,使用如下模式
persistentStoreCoordinator---->mainContext
persistentStoreCoordinator---->privateContext
创建两个不同的Context,共用同一个persistentStoreCoordinator,数据操作在privateContext进行,mainContext监听通知NSManagedObjectContextDidSaveNotification,然后通过这个方法mergeChangesFromContextDidSaveNotification:notification把数据merge过来。
这儿有一篇文章写得还不错。


其它

除了上面这些内容外,Core Data里面同样也可以使用索引index,如下图,需要在xcdatamodeld文件里面选中某个属性,勾选index就可以了。
Image
而另外一个特性transient,它的意思这个属性只会存在于内存中,关闭程序后其就会被还原。


iCloud

其实iCloud我没有细入研究了,他里面有三种存储模式,精力实在有限,而且暂时项目中基本用不上,以后有机会再好好研究一下吧。
//检测是否激活了iCloud
NSURL *url = [manager URLForUbiquityContainerIdentifier:nil];


总结

通过这段时间的研究,总体感觉Core Data非常适合处理一些数据对象之间关系比较复杂的场景(object graph management),但同时又不能单纯地理解为一个O/RM,或者是一个SQLite的封装。其与SQLite之间有很多重合的地方,但其没有SQLite灵活,与轻巧。另外一个就是Core Data的学习成本还是比较高的,如果只是一般的数据库存储数据,不太建议使用Core Data,但如果对象之间关系复杂,例如像城市的公交系统、或者读者、书籍、作者这类问题,Core Data的RelationShips特性的确可以简化很多代码。

另外一个特性是UndoManager,如果有需要,Core Data也是一个不错的选择,至于其它的特性并不是一个必选项,所以有很多人把Core Data直接与SQLite来比较的话,我认为是不恰当的,两者的侧重点其实还是有所不同的。

2015-11-15 13:51239