CoreData多线程实现初探(附源码)

由于历史的原因,项目中的火车票城市列表一直采用的全量更新,即:如果需要增删改某几个城市,则必须把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线程的。

方案一.png
多线程方案之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 <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@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的使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容