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
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时,多线程下应注意的点

  1. 在不同的线程,可以使用任意的context进行读的操作,但是写的操作必须保证所有的object均处于同一个context,并且该context所维护的thread就是当前线程。否则在写的时候会出现概率性的崩溃。一个context不能同时被两个线程访问
  2. 不同线程之间的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];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容