作者:luhui CVTE iOS開發(fā)工程師
前言
最近使用Core data在多線程下遇到了不少的坑,多數(shù)都是因為使用不規(guī)范所引起的,這里將從最常用的MOC做展開照激,談談多線程下如何正確的使用context。
p.s.這篇博客會持續(xù)不斷的更新鳖孤,主要是記錄core data中踩到的坑,在踩坑中慢慢成長抡笼,不斷規(guī)范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.
從字義上來說推姻,他就是一個數(shù)據(jù)庫的上下文平匈,并且維持在內存中。
context可以創(chuàng)建多個,說明一個進程中增炭,可以擁有多個數(shù)據(jù)庫的內存副本忍燥,并且每個副本之間都是相對獨立的。
其實從這一點來看隙姿,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)机久。并且提供了非常豐富的數(shù)據(jù)操作,基本的增查刪改赔嚎,還提供了了undo/redo的方法膘盖,極大的簡化了開發(fā)時間。
fault
moc中的所有object都是lazy loading的尤误,所有注冊到moc的object一開始都是出于faulted的狀態(tài)侠畔,只有在引用到object時才會真正從數(shù)據(jù)庫中加載數(shù)據(jù)至內存中,這個過程叫fire fault损晤。當一個object被從moc中移除后软棺,他會被置為fault狀態(tài)。
重頭戲:Parent Store
parent store是用來告訴MOC沉馆,數(shù)據(jù)的來源码党,數(shù)據(jù)的讀取保存操作最終都是在parent store中執(zhí)行的德崭。
在OSX10.7以及iOS5.0時代斥黑,Parent Store一直是persisten store coordinator
從OSX10.7以及iOS5.0之后,Parent Store就可以是context眉厨,相當于定義了一個parent 這個moc的數(shù)據(jù)操作最終只會影響到其parent context锌奴,這極大的簡化了不同context之間數(shù)據(jù)的更新。在這之前一直只能用Notification的方式:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(<#Selector name#>)
name:NSManagedObjectContextDidSaveNotification
object:<#A managed object context#>];
我們可以在程序運行的斷點中查看context的parent store
這個parent store最終的應用場景是怎樣呢憾股?我們在后邊的Nested context中介紹
MagicalRecord概述
Magical Recrod是git的一個第三方庫鹿蜀,是一個對Core data數(shù)據(jù)操作的封裝庫,常用的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為每個線程都創(chuàng)建了context,并且在線程的threadDictionary
中維持context的引用。因此在線程回收之前分冈,我們都可以放心的在這個線程中使用context圾另,并且不用擔心何時需要釋放context的問題。
在MR中雕沉,初始化core data時集乔,他的數(shù)據(jù)結構是這樣的:
+ (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坡椒。
在主線程創(chuàng)建一個default context扰路,其parent context是root context,因此我們在使用core data時肠牲,在主線程下的操作都是用的是default context幼衰,最終的操作都會是先保存到root context中,再保存到數(shù)據(jù)庫中缀雳。
具體的解釋暫且不表渡嚣,放在另一篇文章中詳細解釋使用MR時的一些操作事項
多線程下的Core Data
MOC的三種類型
Confinement (NSConfinementConcurrencyType)
默認狀態(tài),只能在創(chuàng)建的線程中使用肥印,其他線程均不能使用识椰。官方的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就是當前線程幽七。否則在寫的時候會出現(xiàn)概率性的崩潰景殷。一個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
后,開了一個線程進行了數(shù)據(jù)操作并保存:
dispatch_async(dispatch_get_main_queue(), ^{
//somthing data operation
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveOnlySelfAndWait];
});
室埋,這時候數(shù)據(jù)會先更新至defaultContext中办绝,再保存至數(shù)據(jù)庫踏兜。如果我們只打算undo在主線程中的更改,調用
[[[NSManagedObjectContext MR_defaultContext] undoManager] endUndoGrouping];
[[[NSManagedObjectContext MR_defaultContext] undoManager] undo];
進行回滾八秃,會把線程中執(zhí)行的操作一起回滾掉碱妆。因此這種業(yè)務需求下的undo應該使用另一個MOC進行處理,defaultContext作為最終同時保存的部分昔驱。
NSUndoManager *undoManager = [NSUndoManager new];
NSManagedObjectContext *moc = [NSManagedObjectContext MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
[moc setUndoManager:undoManager];
[[moc undoManager] beginUndoGrouping];