由于历史的原因,项目中的火车票城市列表一直采用的全量更新,即:如果需要增删改某几个城市,则必须把app本地缓存的城市列表全部删除重新从接口拉取一份最新的数据。现在为了优化用户体验,需要改成增量更新的方式。
一开始并没有考虑到多线程的问题,直接使用了项目中封装好的基于CoreData的DBHelper工具类进行开发。QA期间测试进行几千条城市数据的增删改才发现,中途会有2s~3s的卡顿。于是周末花了半天时间学习了下CoreData的多线程开发。
为什么不用GCD包一层异步去处理这些耗时操作?
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
CoreData不是线程安全的,对于ManagedObject以及ManagedObjectContext的访问,都只能在MOC初始化所指定的线程上进行(NSMainQueueConcurrencyType、NSPrivateQueueConcurrencyType),而不能跨线程。
对于CoreData的多线程,苹果有自己的一套解决方案,并不能简单的将MOC从一个线程中传递到另一个线程中使用。
多种方案简介
多线程方案之child/parent context
ChildContext和ParentContext是相互独立的。只有当ChildContext中调用save了以后,才会把这段时间来Context的变化提交到ParentContext中,ChildContext并不会直接提交到NSPersistentStoreCoordinator中, parentContext就相当于它的NSPersistentStoreCoordinator。
注意:子线程的save操作并没有任何关于Disk IO的操作。而后mainContext在UI线程又要执行一次save操作才能真正将数据变动写进数据库中,这里的save操作就与Disk IO有关了,而且又是在主线程,所以说这种设计是最阻碍UI线程的。
多线程方案之Notification
简单来说,就是不同的线程使用不同的context进行操作,当一个线程的context发生变化后,利用notification来通知另一个线程Context,另一个线程调用mergeChangesFromContextDidSaveNotification
来合并变化。
persistentStoreCoordinator<-mainContext, persistentStoreCoordinator<-privateContext
首先在ViewController中要添加一个名为NSManagedObjectContextDidSaveNotification的通知
,然后子线程中创建privateContext,进行数据增删改查操作,直接save到本地数据库,这时在ViewController中会回调之前注册的NSManagedObjectContextDidSaveNotification的回调方法,在该方法中调用mainContext的mergeChangesFromContextDidSaveNotification:notification方法,将所有的数据变动merge到mainContext中,这样就保持了两个Context中的数据同步。由于大部分的操作都是privateContext在子线程中操作的,所以这种设计是UI线程耗时最少的一种设计,但是它的代价是需要多写mergeChanges的方法。(注:前两种parent,child的Context,一旦childContext调用save方法,其parentContext不用任何merge操作,CoreData自动将数据merge到parentContext当中)
多线程方案之改良版child/parent context(本文实现)
理清关系
persistentStoreCoordinator<-backgroundContext<-mainContext<-privateContext
这种设计是第一种的改进设计,也是上述的老外博主推荐的一种设计方式。它总共有三个Context,一是连接persistentStoreCoordinator也是最底层的backgroundContext,二是UI线程的mainContext,三是子线程的privateContext,他们的关系如下:
privateContext.parentContext = mainContext, mainContext.parentContext = backgroundContext。
工作流程
在应用中,如果我们有API操作,首先我们会起一个子线程进行API请求,在得到Response后要进行数据库操作,这是我们要创建一个privateContext进行数据的增删改查,然后call privateContext的save方法进行存储,这里的save操作只是将所有数据变动Push up到它的父Context中也就是mainContext中,然后mainContext继续call save方法,将数据变动Push up到它的父Context中也就是backgroundContext,最后调用backgroundContext的save方法真正将数据变动存储到Disk数据库中,在这个过程中,前两个save操作相对耗时较少,真正耗时的操作是最后backgroundContext的save操作,因为只有它有Disk IO的操作。
干货
三个Context的初始化
appDelegate.m中的实现
-(NSManagedObjectContext *)rootObjectContext {//对应上述backgroundContext
if (nil != _rootObjectContext) {
return _rootObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_rootObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_rootObjectContext setPersistentStoreCoordinator:coordinator];
}
return _rootObjectContext;
}
- (NSManagedObjectContext *)managedAsyncObjectContext
{//对应mainContext
if (_managedAsyncObjectContext != nil) {
return _managedAsyncObjectContext;
}
_managedAsyncObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedAsyncObjectContext.parentContext = [self rootObjectContext];
return _managedAsyncObjectContext;
}
- (NSManagedObjectContext *)privateAsyncObjectContext
{
if (_privateAsyncObjectContext != nil) {
return _privateAsyncObjectContext;
}
_privateAsyncObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateAsyncObjectContext setParentContext:[self managedAsyncObjectContext]];
return _privateAsyncObjectContext;
}
mainContext的save方法
AppDelegate中saveContext方法,每次privateContext调用save方法成功之后都要call这个方法
- (void)saveAsyncContextWithWait:(BOOL)needWait
{
NSManagedObjectContext *managedAsyncObjectContext = [self managedAsyncObjectContext];
NSManagedObjectContext *rootObjectContext = [self rootObjectContext];
if (nil == managedAsyncObjectContext) {
return;
}
if ([managedAsyncObjectContext hasChanges]) {
NSLog(@"Main context need to save");
[managedAsyncObjectContext performBlockAndWait:^{
NSError *error = nil;
if (![managedAsyncObjectContext save:&error]) {
NSLog(@"Save main context failed and error is %@", error);
}
}];
}
if (nil == rootObjectContext) {
return;
}
if ([rootObjectContext hasChanges]) {
NSLog(@"Root context need to save");
if (needWait) {
[rootObjectContext performBlockAndWait:^{
NSError *error = nil;
if (![_rootObjectContext save:&error]) {
NSLog(@"Save root context failed and error is %@", error);
}
}];
}
else {
[rootObjectContext performBlock:^{
NSError *error = nil;
if (![_rootObjectContext save:&error]) {
NSLog(@"Save root context failed and error is %@", error);
}
}];
}
}
}
CoreData多线程工具类DBAsyncHelper
#import
#import
@interface DBAsyncHelper : NSObject
typedef void (^ DBCompletionBlock) (BOOL operationSuccess, id responseObject, NSString *errorMessage);
/*********************************************************
函数名称:startWithTimeConsumingOperation
函数描述:子线程处理耗时DB操作(使用本类必须调此方法发起DB操作)
输入参数:operationBlock :数据处理代码块 completionBlock :成功回调(主要用于刷新UI)
输出参数:无
返回值:无
**********************************************************/
+ (void)startWithTimeConsumingOperation:(void(^)())operationBlock completionBlock:(DBCompletionBlock)completionBlock;
/*********************************************************
函数名称:asyncSearchBy
函数描述:根据ModelName查询记录
输入参数:ModelEmptyName :数据库表名
输出参数:查询结果
返回值:NSMutableArray
**********************************************************/
+(NSMutableArray*)asyncSearchBy:(NSString*)ModelEmptyName;
/*********************************************************
函数名称:asyncSearchWithPredicateByEntity
函数描述:自定义二级查询
输入参数:modelEntityName:Entity名 predicate:查询条件(NSPredicate对象)
输出参数:查询结果
返回值:NSMutableArray
**********************************************************/
+(NSMutableArray*)asyncSearchWithPredicateByEntity:(NSString*)modelEntityName withKeys:(NSPredicate*) predicate;
/*********************************************************
函数名称:asyncInsertWithEntity
函数描述:根据Model名称获得一个数据库实例对象
输入参数:ModelEmptyName:数据库表名
输出参数:数据库实例对象
返回值:id
**********************************************************/
+(id)asyncInsertWithEntity:(NSString*)ModelEntityName;
+(void)asyncDeleteWithOutSaveBy:(id)obj;
+(Boolean)Save;
@end
DBAsyncHelper.m具体实现
#import "DBAsyncHelper.h"
#import "AppDelegate.h"
#import "TCFoundation.h"
@implementation DBAsyncHelper
+ (void)startWithTimeConsumingOperation:(void(^)())operationBlock completionBlock:(DBCompletionBlock)completionBlock
{
NSManagedObjectContext *context = [AppDelegateEntity privateAsyncObjectContext];
[context performBlock:^{
operationBlock();
NSError * error = nil;
if ([context save:&error]) {
[AppDelegateEntity saveAsyncContextWithWait:YES];
completionBlock(YES,nil,nil);
} else {
NSString *sError = [NSString stringWithFormat:@"%@",error.localizedDescription];
NSDebugLog(@"%@",sError);
completionBlock(NO,nil,sError);
}
}];
}
+(NSMutableArray*)asyncSearchBy:(NSString*)ModelEmptyName
{
NSManagedObjectContext* context = [AppDelegateEntity privateAsyncObjectContext];
NSFetchRequest* request = [[NSFetchRequest alloc] init];
NSEntityDescription* entityDesc = [NSEntityDescription entityForName:ModelEmptyName inManagedObjectContext:context];
[request setEntity:entityDesc];
NSError* error;
NSArray* objects = [[NSArray alloc] initWithArray:[context executeFetchRequest:request error:&error]];
NSMutableArray* mutableObjects = [[NSMutableArray alloc] initWithArray:objects];
return mutableObjects;
}
+(NSMutableArray*)asyncSearchWithPredicateByEntity:(NSString*)modelEntityName withKeys:(NSPredicate*) predicate
{
NSManagedObjectContext *context = [AppDelegateEntity privateAsyncObjectContext];
NSFetchRequest* request = [[NSFetchRequest alloc] init];
NSEntityDescription* entityDesc = [NSEntityDescription entityForName:modelEntityName inManagedObjectContext:context];
[request setEntity:entityDesc];
[request setPredicate:predicate];
NSError* error;
NSArray* objects = [[NSArray alloc] initWithArray:[context executeFetchRequest:request error:&error]];
NSMutableArray *resultObjects = [[NSMutableArray alloc] initWithArray:objects];
return resultObjects;
}
+(id)asyncInsertWithEntity:(NSString*)ModelEntityName
{
NSManagedObjectContext *context = [AppDelegateEntity privateAsyncObjectContext];
id obj = [NSEntityDescription insertNewObjectForEntityForName:ModelEntityName inManagedObjectContext:context];
return obj;
}
+(void)asyncDeleteWithOutSaveBy:(id)obj
{
NSManagedObjectContext *context = [AppDelegateEntity privateAsyncObjectContext];
[context deleteObject:obj];
}
+(Boolean)Save
{
NSManagedObjectContext *context = [AppDelegateEntity privateAsyncObjectContext];
NSError* error = nil;
if (![context save:&error])
{
return NO;
}
return YES;
}
DBAsyncHelper使用示例
//子线程更新站点
- (void)asyncUpdateStationList
{
[DBAsyncHelper startWithTimeConsumingOperation:^{
//使用DBAsyncHelper中相关增删方法进行数据库耗时操作
} completionBlock:^(BOOL operationSuccess, id responseObject, NSString *errorMessage) {
//handle result
}];
}
备注
本文重点在于整理出一种可以直接使用的CoreData多线程解决方案,学习过程中参考了以下文章:
iOS CoreData详解(五)多线程
CoreData 多线程下NSManagedObjectContext的使用