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推荐的方法保持一致。不仅如此,我们还提供了很多的样板代码可以让你直接在你的应用中安全可靠的使用。