7

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

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

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

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

主线程后台线程
viewDidLoad

                                     获取 JSON 数据(使用 AFNetworking)

创建子 NSManagedObjectContext (MOC)

                                     解析 JSON 数据
                                     在子 MOC 中插入托管对象
                                     拯救儿童 MOC
                                     导入后完成通知

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

                                     删除子 MOC 中的旧托管对象
                                     拯救儿童 MOC
                                     删除后完成通知

接收删除完成通知
保存父 MOC

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

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

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

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

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

4

4 回答 4

3

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

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

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

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

于 2012-07-04T20:56:02.673 回答
2

Just an architectural idea:

With your stated data refresh pattern (once a day, FULL cycle of data deleted and added), I would actually be motivated to create a new persistent store each day (i.e. named for the calendar date), and then in the completion notification, have the table view setup a new fetchedresultscontroller associated with the new store (and likely a new MOC), and refresh using that. Then the app can (elsewhere, perhaps also triggered by that notification) completely destroy the "old" data store. This technique decouples the update processing from the data store that the app is currently using, and the "switch" to the new data might be considered dramatically more atomic, since the change happens simply be starting to point to the new data instead of hoping you aren't catching the store in an inconsistent state while new data is being written (but is not yet complete).

Obviously I have left some details out, but I tend to think that much data being changed while being used should be re-architected to reduce the likelihood of the kind of crash you are experiencing.

Happy to discuss further...

于 2012-07-04T20:51:01.120 回答
2

NSFetchedResultsController已被证明对大量删除有点敏感,所以这是我首先开始挖掘的地方。

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

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

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

实际上,不是使用父 MOC 作为主线程并发类型,而是UIManagedDocument将主 MOC 创建为私有队列,并使子 MOC 可供您在主线程上使用。这样做的好处是所有 I/O 在后台进行并保存到父 MOC 根本不会干扰子 MOC,直到子 MOC 明确知道它们。那是因为 save 提交从子到父的更改,而不是相反。

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

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

主 MOC ( NSPrivateQueueConcurrencyType)

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

儿童 MOC A ( NSMainQueueConcurrencyType)

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

子 MOC B ( NSPrivateQueueConcurrencyType, 子 MOC A 的子)

  • 负责插入新数据并在完成后将其提交给子 MOC A。
于 2012-07-05T06:17:15.277 回答
2

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

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

这将涉及继承 NSManagedObjectContext 和 NSManagedObject:

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

这只是几行代码,但从长远来看,可以防止您犯下难以追踪的错误。

于 2012-07-04T21:12:00.183 回答