同时可靠地使用核心数据

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

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

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

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

 主线程背景线程
 viewDidLoad中

                                     获取JSON数据(使用AFNetworking)

创建子NSManagedObjectContext(MOC)

                                     解析JSON数据
                                     在子MOC中插入托管对象
                                     救孩子MOC
                                     发布导入完成通知

接收导入完成通知
保存父MOC
执行获取和重新加载表视图

                                     删除子MOC中的旧托管对象
                                     救孩子MOC
                                     发布删除完成通知

接收删除完成通知
保存父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上执行保存。

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

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

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

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

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

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

只是一个建筑理念:

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

显然我已经留下了一些细节,但我倾向于认为在使用时要更改的大量数据应该重新设计,以减少您遇到的那种崩溃的可能性。

很高兴进一步讨论……

NSFetchedResultsController已被certificate对大量删除有点敏感,因此我将首先开始挖掘。

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

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

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

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

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

我提供的另一种选择是使用三种上下文:

主要MOCNSPrivateQueueConcurrencyType

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

Child MOC ANSMainQueueConcurrencyType

  • 负责与UI相关的任何事情和NSFetchedResultsController

儿童MOC BNSPrivateQueueConcurrencyType ,Child MOC A的孩子)

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

我对multithreading核心数据的主要问题是无意中访问了一个线程/队列中的托管对象,而不是它创建的那个。

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

这将涉及子类化NSManagedObjectContext和NSManagedObject:

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

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