CoreData与MagicalRecord的故事
作者:luhui CVTE iOS开发工程师
前言
最近使用Core data在多线程下遇到了不少的坑,多数都是因为使用不规范所引起的,这里将从最常用的MOC做展开,谈谈多线程下如何正确的使用context。
p.s.这篇博客会持续不断的更新,主要是记录core data中踩到的坑,在踩坑中慢慢成长,不断规范core data的使用方法。
NSManagedObjectContext
定义
说到MOC,当然先要贴上一副描述
An instance of NSManagedObjectContext represents a single “object space” or scratch pad in an application. Its primary responsibility is to manage a collection of managed objects. These objects form a group of related model objects that represent an internally consistent view of one or more persistent stores. A single managed object instance exists in one and only one context, but multiple copies of an object can exist in different contexts. Thus object uniquing is scoped to a particular context.
从字义上来说,他就是一个数据库的上下文,并且维持在内存中。
context可以创建多个,说明一个进程中,可以拥有多个数据库的内存副本,并且每个副本之间都是相对独立的。
其实从这一点来看,Core data的设计结构和git是相似的。persistent store类似git的中心仓库,每个moc都是git的分支,moc内的操作都互不影响,只有在save(git的push)时才会保存到persistent store(git中心仓库)中。
生命周期
The context is a powerful object with a central role in the life-cycle of managed objects, with responsibilities from life-cycle management (including faulting) to validation, inverse relationship handling, and undo/redo. Through a context you can retrieve or “fetch” objects from a persistent store, make changes to those objects, and then either discard the changes or—again through the context—commit them back to the persistent store. The context is responsible for watching for changes in its objects and maintains an undo manager so you can have finer-grained control over undo and redo. You can insert new objects and delete ones you have fetched, and commit these modifications to the persistent store.
All objects fetched from an external store are registered in a context together with a global identifier (an instance of NSManagedObjectID) that’s used to uniquely identify each object to the external store.
作为对象的管理者,moc管理着所有注册在这个moc下的对象的生命周期(包括faulting)。并且提供了非常丰富的数据操作,基本的增查删改,还提供了了undo/redo的方法,极大的简化了开发时间。
fault
moc中的所有object都是lazy loading的,所有注册到moc的object一开始都是出于faulted的状态,只有在引用到object时才会真正从数据库中加载数据至内存中,这个过程叫fire fault。当一个object被从moc中移除后,他会被置为fault状态。
重头戏:Parent Store
parent store是用来告诉MOC,数据的来源,数据的读取保存操作最终都是在parent store中执行的。
在OSX10.7以及iOS5.0时代,Parent Store一直是persisten store coordinator
从OSX10.7以及iOS5.0之后,Parent Store就可以是context,相当于定义了一个parent 这个moc的数据操作最终只会影响到其parent context,这极大的简化了不同context之间数据的更新。在这之前一直只能用Notification的方式:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(<#Selector name#>)
name:NSManagedObjectContextDidSaveNotification
object:<#A managed object context#>];
我们可以在程序运行的断点中查看context的parent store
img这个parent store最终的应用场景是怎样呢?我们在后边的Nested context中介绍
MagicalRecord概述
Magical Recrod是git的一个第三方库,是一个对Core data数据操作的封装库,常用的core data操作都得到了极大的简化,是在使用core data时提高编程效率的不二之选。
除开对操作的封装之外,MR的另一大好处就是为我们封装了多线程之下的context
+ (NSManagedObjectContext *) MR_contextForCurrentThread;
{
if ([NSThread isMainThread])
{
return [self MR_defaultContext];
}
else
{
NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary];
NSManagedObjectContext *threadContext = [threadDict objectForKey:kMagicalRecordManagedObjectContextKey];
if (threadContext == nil)
{
threadContext = [self MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
[threadDict setObject:threadContext forKey:kMagicalRecordManagedObjectContextKey];
}
return threadContext;
}
}
从他的代码,可以很清楚的知道,MR为每个线程都创建了context,并且在线程的threadDictionary
中维持context的引用。因此在线程回收之前,我们都可以放心的在这个线程中使用context,并且不用担心何时需要释放context的问题。
在MR中,初始化core data时,他的数据结构是这样的:
+ (void) MR_initializeDefaultContextWithCoordinator:(NSPersistentStoreCoordinator *)coordinator;
{
if (defaultManagedObjectContext_ == nil)
{
NSManagedObjectContext *rootContext = [self MR_contextWithStoreCoordinator:coordinator];
[self MR_setRootSavingContext:rootContext];
NSManagedObjectContext *defaultContext = [self MR_newMainQueueContext];
[self MR_setDefaultContext:defaultContext];
[defaultContext setParentContext:rootContext];
}
}
一个root context,所有的context都会是这个root context的child context。
在主线程创建一个default context,其parent context是root context,因此我们在使用core data时,在主线程下的操作都是用的是default context,最终的操作都会是先保存到root context中,再保存到数据库中。
具体的解释暂且不表,放在另一篇文章中详细解释使用MR时的一些操作事项
多线程下的Core Data
MOC的三种类型
Confinement (NSConfinementConcurrencyType)
默认状态,只能在创建的线程中使用,其他线程均不能使用。官方的API文档写到:
You can only use this concurrency type if the managed object context’s parent store is a persistent store coordinator.
Private queue (NSPrivateQueueConcurrencyType)
用在多线程下。MR中的rootContext与threadContext均是这个类型
Main queue (NSMainQueueConcurrencyType)
用在主线程下。MR中的defaultContext是这个类型。这个类型的context多用在controller以及一些UI对象上,这些对象只能要求在主线程中操作,因此只能用此类型的context在主线程中操作。
使用Magical Recrod时,多线程下应注意的点
- 在不同的线程,可以使用任意的context进行读的操作,但是写的操作必须保证所有的object均处于同一个context,并且该context所维护的thread就是当前线程。否则在写的时候会出现概率性的崩溃。一个context不能同时被两个线程访问
- 不同线程之间的object传递使用objectWithId。在MR中,使用的是existingObjectWithId,也就是已经在context中注册的object才可以拿到
NSManagedObjectContext *moc = [NSManagedObjectContext MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
Department *d = [Department MR_createInContext:moc];
NSManagedObjectContext *moc2 = [NSManagedObjectContext MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
Department *t = [d MR_inContext:moc2];
Department *t2 = (Department *)[moc2 objectWithID:d.objectID];
NSLog(@"t:%@ and t2:%@", t, t2);
这个代码的输出结果为
t:(null) and t2:<Department: 0x109600580> (entity: Department; id: 0x10f507590 <x-coredata:///Department/t8F610E34-B04A-4152-A481-75818DEC7DE12> ; data: <fault>)
3.多个context下,使用undo操作时就要慎用,有可能会多undo了某些操作。比如:
NSUndoManager *undoManager = [NSUndoManager new];
[[NSManagedObjectContext MR_defaultContext] setUndoManager:undoManager];
[[[NSManagedObjectContext MR_defaultContext] undoManager] beginUndoGrouping];
当在beginUndoGrouping
后,开了一个线程进行了数据操作并保存:
dispatch_async(dispatch_get_main_queue(), ^{
//somthing data operation
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveOnlySelfAndWait];
});
,这时候数据会先更新至defaultContext中,再保存至数据库。如果我们只打算undo在主线程中的更改,调用
[[[NSManagedObjectContext MR_defaultContext] undoManager] endUndoGrouping];
[[[NSManagedObjectContext MR_defaultContext] undoManager] undo];
进行回滚,会把线程中执行的操作一起回滚掉。因此这种业务需求下的undo应该使用另一个MOC进行处理,defaultContext作为最终同时保存的部分。
NSUndoManager *undoManager = [NSUndoManager new];
NSManagedObjectContext *moc = [NSManagedObjectContext MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
[moc setUndoManager:undoManager];
[[moc undoManager] beginUndoGrouping];