不再为Core Data多线程编程而头疼

by Saul Mora原文链接:http://www.cimgf.com/2011/05/04/core-data-and-threads-without-the-headache/

我知道我曾经提到我要写一篇关于定制fetch requests的文章,然而,在我为Active Record Fetching project(现在已经改名为MagicalRecord)编写了一些代码之后,我觉得写一篇关于fetching–threading.的文章更好一些。

当大多数cocoa开发者提到Core Data多线程编程时,我见到最经常的反应是神秘和怀疑。其一,多线程程序真的很难很难设计正确,编写正确,而调试多线程程序简直是自找麻烦。而引入Core Data似乎可以算是压垮骆驼的最后一根稻草。然而,如果遵循几条简单的规则和导引,并将它们编撰成一个简单的模式(这个模式你也许特别熟悉),我们就可以毫不费力的编写出安全的Core Data多线程程序。

另外提醒:这篇文章的例子请参考github上面的MagicalRecord开源项目。这个项目基本上是在标准的Core Data API上扩展了一些类以及类别

苹果是怎么做的

为了建立Core Data并在不同的线程中传递数据对象并在后台线程中执行保存操作,我们看一下官方文档是怎么做的。总结一下要点:

1.      每个线程必须有自己的NSManagedObjectContext

2.      NSManagedObjectContext并不是线程安全的

3.      NSManagedObjectContextID是线程安全的

4.      如果你在后台线程中保存数据,你需要将你的改变使用Core Data的系统通知NSManagedObjectContextDidSaveNotification告诉其他Context合并这些改变

那么在代码中应该怎么做呢?最常用的一个解决方案是创建一个自定义的NSOperation,然后在这个operation的main方法中创建我们的context,代码如下:

#import "MGPCoreDataOperation.h"

@interface MGPCoreDataOperation ()

@property (nonatomic, retain) NSManagedObjectContext *context;

@end

@implementation MGPCoreDataOperation

- (void)main

{

[selfsetContext:[NSManagedObjectContext context]];

//perform my core dataoperations here

}

@end

注意你是在你自定义NSOperation的main方法中创建的NSManagedObjectContext对象的,而不是在init方法中。main函数中的context是唯一一个可以授权其他线程调用的,你的第二个NSManagedObjectContext是在将要被使用时才创建的。

这个解决方案是很直观的,然而,跳出这个类想一下。这是一个有效的方案,然而使用这个方案,重用是很笨重的,对每个save操作你都必须:

1.      每次都需要继承类或者使用模板创建这个类

2.      在主线程中初始化这个对象,并将其加入NSOperationQueue队列

3.      需要通过一个属性传递对象ID

对于在后台解析数据并在解析完成之后立即保存数据的应用来说,这个解决方案十分笨重。让我们使用OC-block和GCD技术尝试一种比较简洁的方案,至少看上去比较容易。

一点准备知识

在我们进行线程冒险之前,为了更好的理解,我们先讲解一点有关线程的知识。在线程和UI的情景中,我们需要明确主线程的概念。主线程和其他线程不同,它被称为前台线程,是执行主循环的线程,也就是说,处理外界发给应用的点击触摸等事件,并将这些事件传递给你的应用的线程。从这个例子中,让我们引入住MOC的概念。这个MOC只能被主线程调用,这个MOC总是可访问的,就像主线程和主循环一样。在MagicalRecord中,我已经实现了一个defaultContext 类方法,可以在任何地方访问这个MOC。

在XCode默认的Core Data工程模板中,重用的MOC对象是APP delegate 的一个属性,这样APPDelegate和MOC对象都可以认为是单例的。在magicalRecord中,保存了这一思想,把它默认的context放在一个单一的地方,所以在应用中很容易就可以引用到,这类似于单例模式。可是,和应用程序模板中的实现不同,它不在App Delegate中保持这个引用。从技术上讲,仅仅是移动了一个引用的位置而已,但是,我的引用去我可以很好的和应用解耦合。

Main context具有一些很灵巧的特性。第一,它可以在需要context参数的时候简化我在前面的博客中提到的查询方法。第二,这可以让我们快速的进行Core Data编程,因为我们可以使用main context在主线程中获取数据,而不用处理跨线程的问题。第三,也是最重要的,我们有了一个可以合并其他所有后台线程改变的context。如果这些好处还不足够,还有一条,就是通过KVO被观察的MO实例的改变可以立即反馈给UI组件,这样UI就可以刷新数据了。

Core Data, Blocks and GCD…oh my!

当你需要在应用中保存大量数据时,Core Data的多线程操作就很有必要了。因为复杂的查询会消耗很多时间,更大的瓶颈是在保存操作上。因此,这个解决方案是使得后台保存操作简易而安全。

从一个进行View动画的最新的IOS API中获得灵感,我们找到了基于block的解决方案。Blocks难以置信的强大,特别针对核心动画编程,极大的简化了代码。一个普通的动画代码如下所示:

UIView *redView = ...;

[UIView animateWithDuration:5.0

animations:^{

redView.alpha = 0.0;

}

];

我们可以使用这个范例在后台线程中保存数据吗?如果我们为save操作提供一个像动画这样的API会如何?首先让我们考虑一下同步的问题,这个API会设计成下面这个样子

+ (void) saveDataInContext:(void(^)(NSManagedObjectContext*context))saveBlock;

可以像如下这个例子那样使用这个API:

PersonEntity *person = ...;

NSManagedObjectID *objectID = [person objectID];

[NSManagedObjectHelper saveDataInContext:^(NSManagedObjectContext*localContext){

PersonEntity*localPerson = (PersonEntity *)[localContext objectWithID:objectID];

//makemy updates to localPerson here

localPerson.name= @"IronMan";

//...morechanges

}];

这个例子并不是特别重要因为这都是在一个线程中运行的。我必须提醒你NSManagedObjects并不是线程安全的,但是NSManagedObjectID是线程安全呢的。因此我们需要线程间传递一个NSManagedObject的object id,或者在这个例程中,就是在block语句块中。

要使得这个例程能够正常工作,saveDataInContext 这个方法的实现应该:

1.      创建一个新的NSManagedObjectContext

2.      为所有的context设置一个合并策略

3.      让主context能够监听到后台context,以便在后台context进行保存的时候进行合并操作

4.      将新创建的那个context作为block的参数以便其他的代码可以使用这个context

这将会创建一个轻量级的save或者update操作,就像创建一个基于block的动画一样简单。

那么,这个方法的方法体是怎样的呢?让我们看一下吧:

+ (void)saveDataInContext:(void(^)(NSManagedObjectContext*context))saveBlock

{

NSManagedObjectContext*context = [NSManagedObjectContext context];                                                                                           //step1

[contextsetMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];                                                                                                         
//step2

[defaultContextsetMergePolicy:NSMergeObjectByPropertyStoreTrumpMergePolicy];

[defaultContextobserveContext:context];                                                       //step3

block(context);                                                                                        //step4

if([context hasChanges])                                                                     //step5

{

[contextsave];  //MagicalRecord will dump errors to the console with this savemethod

}

}、

这段代码使用了MagicalRecord的一些方法,实现涉及到的步骤有:

Step1:创建一个新的context

Step2:设置合并策略

Step3:创建通知

Step4:回调block方法并将我们创建的context传递出去

Step5:如果context有更新,那么保存context

还要注意到,为了使合并自动进行,一个context必须设置为赢得合并。在这个例子中,我们想要后台线程赢得合并(指的是后台线程的信息时最新的,需要将后天线程中的数据合并到主线程),通过在有冲突的时候告诉主线程在我们的data store中保存的数据就是我们需要的正确的数据。因为我们使用了block,我们还可以使用GCD的API。

使用GCD编写的代码如下:

+ (void)saveDataInBackgroundWithContext:(void(^)(NSManagedObjectContext*context))saveBlock

{

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^{

[selfsaveDataInContext:saveBlock];

});

}

Going the Distance

但是,我们可以更进一步吗?UIView动画API最灵活的特性是什么?就是当动画完成后可以调用block的能力。如果我们能够在后台线程保存完数据并且已经合并完成之后提供一个block回调给程序员会怎样?为了激发灵感,让我们看一下UIView 动画的接口:

+(void)animateWithDuration:(NSTimeInterval)<em>duration</em>animations:(void (^)(void))<em>animations</em> completion:(void(^)(BOOL finished))<em>completion</em>

使用这个API的例子为:

UIView *redView = ...;

[UIView animateWithDuration:5.0

animations:^{

redView.alpha= 0.0;

}

completion:^(BOOL  completed){

[redViewremoveFromSuperview];

[redViewrelease];

redView= nil;

}

];

为了安全起见,我们需要确保completion block在主线程中调用

+ (void)saveDataInBackgroundWithContext:(void(^)(NSManagedObjectContext*context))saveBlock completion:(void(^)(void))completion

{

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^{

[selfsaveDataInContext:saveBlock];

dispatch_sync(dispatch_get_main_queue(), ^{

completion();

});

});

}

现在,你在一个地方保存,重新加载或者更新数据了,就像这样:

NSArray *listOfPeople = ...;

[NSManagedObjectHelpersaveDataInBackgroundWithContext:^(NSManagedObjectContext *localContext){

for(NSDictionary *personInfo in listOfPeople)

{

PersonEntity*person = [PersonEntity createInContext:localContext];

[personsetValuesForKeysWithDictionary:personInfo];

}

} completion:^{

self.people= [PersonEntity findAll];

}];

这段代码做了很多的工作

1.      创建了一个后台使用的MOC

2.      创建了一个后台的GCD队列

3.      保存后台MOC,并通知主线程合并数据

4.      当改变已经保存到持久化存储之后在主线程中回调completion block,这样就可以在这个block中进行一些操作了

然而,还可以做一件事情可以让save操作更简洁。现在,后台线程是在一个全局后台队列(global background queue)中进行的,我已经几次遇到过需要为Core Data操作建立一个单独的专用的GCD队列的地方。在MagicalRecord中,我们定义了一个静态的方法让我们访问这个专用的GCD队列

static dispatch_queue_t coredata_background_save_queue;

dispatch_queue_t background_save_queue()

{

if(coredata_background_save_queue == NULL)

{

coredata_background_save_queue =dispatch_queue_create("com.magicalpanda.coredata.backgroundsaves",0);

}

returncoredata_background_save_queue;

}

同步修改后台core data代码如下:

+ (void)saveDataInBackgroundWithContext:(void(^)(NSManagedObjectContext*context))saveBlock completion:(void(^)(void))completion

{

dispatch_async(coredata_background_save_queue(),^{

[selfsaveDataInContext:saveBlock];

dispatch_sync(dispatch_get_main_queue(),^{

completion();

});

});

}

使用blocks,GCD和一点点的创造性,我们已经让Core Data多线程编程变得很简单并且能够和Apple推荐的方法保持一致。不仅如此,我们还提供了很多的样板代码可以让你直接在你的应用中安全可靠的使用。

时间: 2024-10-09 22:57:20

不再为Core Data多线程编程而头疼的相关文章

iOS Core data多线程并发访问的问题

大家都知道Core data本身并不是一个并发安全的架构:不过针对多线程访问带来的问题,Apple给出了很多指导:同时很多第三方的开发者也贡献了很多解决方法.不过最近碰到的一个问题很奇怪,觉得有一定的特殊性,与大家分享一下. 这个问题似乎在7.0.1以前的版本上并不存在:不过后来我升级版本到了7.0.4.app的模型很简单,主线程在前台对数据库进行读写,而后台线程不断地做扫描(只读).为此每个线程中各创建了一个NSManagedObjectContext. 这个模型其实有点奇怪,因为普遍的模型是

正确使用Core Data多线程的3种方式

在#Pragma Conference 2015会议上,Marcus Zarra,撰写过关于Core Data和Core Animation的书,叙述了三种在多线程环境下使用Core Data的方法并且设法解决在2015年应如何使用Core Data的问题.实际上,Zarras说道,当用一个拥有十一年历史的技术比如Core Data工作时,你所面临的问题之一是有大量的信息是可用的,不过查明哪一份信息依旧精确以及哪一份不精确并不是一件简单的事. 根据Zarras所言,当我们知道我们仍旧有空余的CP

我为什么用 SQLite 和 FMDB 而不用 Core Data

转:http://segmentfault.com/a/1190000000363392 编者注:文章的"我"是指原作者. 凭良心讲,我不能告诉你不去使用Core Data.它不错,而且也在变好,并且它被很多其他Cocoa开发者所理解,当有新人加入你的组或者需要别人接手你的项目的时候,这点很重要.更重要的是,不值得花时间和精力去写自己的系统去代替它.真的,使用Core Data吧. 为什么我不使用Core Data Mike Ash写到: 就我自己而言,我不是个狂热粉丝.我发现API是

多线程在Core Data中的使用

我们知道,Core Data是线程不安全的.我们不能在不同的线程中共享同一个NSManagedObject和NSManagedObjectContext对象.NSManagedObjectContext对象的创建和使用必须在同一个线程中. 当我们使用NSOperation来实现对Core Data的多线程操作的时候,这里要注意的是NSOperation 的init方法是在调用线程执行的,而start和main方法才是在NSOperation所在线程中执行的,因此要在子线程中创建NSManaged

Core Data中的多线程之二

在Core Data中使用多线程一般不是为了能够提高性能和效率,而是为了使主线程能够不被阻塞,使能够在做其他数据操作的时候,UI还能够继续响应用户的行为.当执行fetch操作时,Core Data系统会根据需要自动开启多个线程做相应的操作,因此我们自己添加多线程并不能提高效率,而仅仅是为了能够将主线程丛繁重的数据操作中解脱出来. 苹果官方文档中对Core Data并发操作的说明: Concurrency with Core Data: 1.Use Thread Confinement to Su

zz 说说iOS的多线程Core Data

Core Data是iOS中很重要的一个部分,可以理解为基于SQLite(当然也可以是其他的Storage,如In-memory,只是SQLite比较常见)的一个ORM实现,所以有关系数据库的特性,又不用写SQL.顺便吐一下槽,官方说法是使用Core Data能减少50%-70%的代码量,但相信用过的人应该都心里明白,Core Data使用起来还是比较麻烦的,这也是为什么有不少的第三方类库来代替/二次包装Core Data. 稍微复杂的应用就有可能出现同时处理多份数据的情况,这就需要用到多线程C

线程同步-iOS多线程编程指南(四)-08-多线程

首页 编程指南 Grand Central Dispatch 基本概念 多核心的性能 Dispatch Sources 完结 外传:dispatch_once(上) Block非官方编程指南 基础 内存管理 揭开神秘面纱(上) 揭开神秘面纱(下) iOS多线程编程指南 关于多线程编程 线程管理 Run Loop 线程同步 附录 Core Animation编程指南 Core Animation简介 基本概念 渲染架构 几何变换 查看目录 中文手册/API ASIHTTPRequest Openg

【转】Linux下的多线程编程

1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的 Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程.现在,多线程技术已经被许多操作系统所支持,包括Windows也包括Linux.  为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题.  使用多线程的理由之一是和进程相比,它是一种非

多线程编程4 - GCD

一.简介 在iOS所有实现多线程的方案中,GCD应该是最有魅力的,因为GCD本身是苹果公司为多核的并行运算提出的解决方案.GCD在工作时会自动利用更多的处理器核心,以充分利用更强大的机器.GCD是Grand Central Dispatch的简称,它是基于C语言的.如果使用GCD,完全由系统管理线程,我们不需要编写线程代码.只需定义想要执行的任务,然后添加到适当的调度队列(dispatch queue).GCD会负责创建线程和调度你的任务,系统直接提供线程管理 二.调度队列(dispath qu