同时可靠地使用核心数据

时间:2012-07-04 20:39:37

标签: iphone ios cocoa-touch core-data nsmanagedobjectcontext

我正在构建我的第一个iOS应用程序,理论上它应该非常简单,但我很难让它足够防弹,让我有信心将它提交到App Store。

简而言之,主屏幕具有表格视图,在选择行时,它将分段到另一个表格视图,该视图以主 - 细节方式显示与所选行相关的信息。基础数据每天从Web服务检索为JSON数据,然后缓存在Core Data存储中。删除当天之前的数据以阻止SQLite数据库文件无限增长。所有数据持久性操作都使用Core Data执行,NSFetchedResultsController支持详细信息表视图。

我看到的问题是,如果您在主数据屏幕和详细信息屏幕之间快速切换几次,同时检索,解析和保存新数据,应用程序会冻结或完全崩溃。似乎存在某种竞争条件,可能是由于Core Data在后台导入数据而主线程正在尝试执行获取,但我猜测。我在捕获任何有意义的崩溃信息时遇到了麻烦,通常它是Core Data堆栈中的一个SIGSEGV。

下表显示了加载详细信息表视图控制器时发生的事件的实际顺序:

Main Thread                          Background Thread
viewDidLoad

                                     Get JSON data (using AFNetworking)

Create child NSManagedObjectContext (MOC)

                                     Parse JSON data
                                     Insert managed objects in child MOC
                                     Save child MOC
                                     Post import completion notification

Receive import completion notification
Save parent MOC
Perform fetch and reload table view

                                     Delete old managed objects in child MOC
                                     Save child MOC
                                     Post deletion completion notification

Receive deletion completion notification
Save parent MOC

在JSON数据到达时触发AFNetworking完成块后,将创建嵌套的NSManagedObjectContext并将其传递给“导入器”对象,该对象解析JSON数据并将对象保存到Core Data存储。导入器使用iOS 5中引入的新performBlock方法执行:

NSManagedObjectContext *child = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [child setParentContext:self.managedObjectContext];        
    [child performBlock:^{
        // Create importer instance, passing it the child MOC...
    }];

导入器对象观察其自己的MOC NSManagedObjectContextDidSaveNotification,然后发布自己的详细信息表视图控制器观察到的通知。发布此通知时,表视图控制器会在其自己的(父)MOC上执行保存。

我使用与“删除”对象相同的基本模式,在导入当天的新数据后删除旧数据。在获取的结果控制器提取新数据并重新加载详细信息表视图后,这会异步发生。

我没有做的一件事是观察任何合并通知或锁定任何托管对象上下文或持久性存储协调器。这是我应该做的事吗?我有点不确定如何正确地构建这一切,所以会感激任何建议。

4 个答案:

答案 0 :(得分:3)

在iOS 5之前,我们通常有两个NSManagedObjectContexts:一个用于主线程,一个用于后台线程。后台线程可以加载或删除数据然后保存。然后将生成的NSManagedObjectContextDidSaveNotification传递给主线程(正如您正在做的那样)。我们调用mergeChangesFromManagedObjectContextDidSaveNotification:将它们带入主线程上下文。这对我们来说效果很好。

这一点的一个重要方面是后台线程上的save:阻塞直到mergeChangesFromManagedObjectContextDidSaveNotification:完成主线程上的运行(因为我们调用mergeChanges ...从听众到那个通知)。这可确保主线程管理对象上下文看到这些更改。如果你有亲子关系,我不知道你是否需要这样做,但你在旧模型中做了以避免各种麻烦。

我不确定在两个上下文之间建立父子关系的好处是什么。从您的描述看来,最终保存到磁盘发生在主线程上,这可能不是出于性能原因的理想选择。 (特别是如果您可能正在删除大量数据;我们的应用程序中删除的主要成本总是在最终保存到磁盘期间发生。)

当控制器出现/消失可能导致核心数据出现问题时,您运行的代码是什么?您看到崩溃的堆栈跟踪是什么类型的?

答案 1 :(得分:2)

只是一个建筑理念:

使用您声明的数据刷新模式(每天一次,删除和添加数据的完整周期),我实际上有动力每天创建一个新的持久性存储(即以日历日期命名),然后在完成通知,让表视图设置一个新的fetchedresultscontroller与新商店(可能是一个新的MOC)相关联,并使用它进行刷新。然后应用程序可以(在其他地方,也许也由该通知触发)完全破坏“旧”数据存储。此技术将更新处理与应用程序当前正在使用的数据存储区分开,并且“切换”到新数据可能会被视为更加 atomic ,因为更改只是开始指向新数据,而不是希望您在写入新数据时(但尚未完成)不会在不一致的状态下捕获存储。

显然我已经遗漏了一些细节,但我倾向于认为在使用时会更改的大量数据应该重新设计,以减少您遇到的崩溃的可能性。< / p>

很高兴进一步讨论......

答案 2 :(得分:2)

我遇到的多线程核心数据的主要问题是无意中访问了创建它之外的线程/队列中的托管对象。

我发现一个好的调试工具是添加NSAsserts来检查在主要托管对象上下文中创建的托管对象是否仅在那里使用,而在后台上下文中创建的托管对象不在主上下文中使用。

这将涉及子类化NSManagedObjectContext和NSManagedObject:

  • 将iVar添加到MOC子类并为其分配创建的队列。
  • 您的MO子类应该检查当前队列是否与其MOC的队列属性相同。

这只是几行代码,但长期可以防止你制作难以追踪的错误。

答案 3 :(得分:2)

NSFetchedResultsController已被证明对大规模删除有点敏感,所以我会先开始挖掘。

我的初步问题是,如何重新获取和重新加载tableview与删除操作的开始有关。删除块是否有可能在NSFetchedResultsController仍在提取或不提取时保存子MOC?

当您从详细视图切换到主视图然后再返回到详细信息视图时,是否可能会运行多个并发后台任务?或者您是否同时从Web服务检索所有数据,而不仅仅是与特定行相关的数据?

使其更加健壮的一种替代方法是使用类似于UIManagedDocument使用的模式:

而不是使用父MOC作为主线程并发类型,UIManagedDocument实际上将主MOC创建为专用队列,并使您可以在主线程上使用子MOC。这里的好处是所有I / O在后台继续进行并保存到父MOC根本不会干扰子MOC,直到明确地让儿童MOC了解它们。那是因为保存提交从子节点变为父节点,而不是相反。

因此,如果您在私有的父队列上执行了删除操作,则根本不会在NSFetchedResultsController范围内进行删除。由于它是旧数据,这实际上是首选方式。

我提供的替代方案是使用三种情境:

主要MOC NSPrivateQueueConcurrencyType

  • 负责持久存储和删除旧数据。

儿童MOC A NSMainQueueConcurrencyType

  • 负责与UI相关的任何内容和NSFetchedResultsController

儿童MOC B NSPrivateQueueConcurrencyType,儿童MOC A的孩子

  • 负责插入新数据并将其提交给Child MOC 完成后。