12

我已经解决了几个星期的问题。每当我保存我的 Core Data 托管对象上下文时,它都会影响 UI 性能。我已经尽我所能,正在寻求一些帮助。

情况

我的应用程序使用两个NSManagedObjectContext实例。一个属于应用程序委托,并附加了一个持久存储协调器。另一个是主 MOC 的子对象,属于一个Class对象,称为PhotoFetcher. 它使用NSPrivateQueueConcurrencyType因此在此 MOC 上执行的所有操作都在后台队列中进行。

我们的应用程序从我们的 API 下载代表照片数据的 JSON 数据。为了从我们的 API 中检索数据,需要执行以下一系列步骤:

  1. 构造一个NSURLRequest对象,使用NSURLConnectionDataDelegate协议构造请求返回的数据,或者处理错误。
  2. JSON 数据下载完成后,在辅助 MOC 的队列上执行以下操作:
    1. NSJSONSerialization使用基础类实例解析 JSON 。
    2. 迭代解析的数据,根据需要在我的后台上下文中插入或更新实体。通常,这会产生大约 300 个新的或更新的实体。
    3. 保存背景上下文。这会将我的更改传播到主 MOC。
    4. 在主 MOC 上执行一个块以保存它的上下文。这是为了让我们的数据持久化到磁盘,一个SQLite存储。最后,对委托进行回调,通知他们响应已完全插入到核心数据存储中。

保存背景 MOC 的代码如下所示:

[AppDelegate.managedObjectContext performBlock:^{
    [AppDelegate saveContext]; //A standard save: call to the main MOC
}];

当主对象上下文保存时,它还保存了自上次保存主对象上下文以来已下载的相当数量的 JPEG。目前,在 iPhone 4 上,我们以 70% 的压缩率下载 15 个 200x200 JPEG,或总共大约 2MB 的数据。

问题

这很有效,而且效果很好。我的问题是,一旦后台上下文保存,NSFetchedResultsController在我的视图控制器中运行就会获取传播到主 MOC 的更改。PSTCollectionView它在我们的 .的开源克隆中插入新的单元格UICollectionView。在插入新单元格时,主上下文会保存这些更改并将其写入磁盘。在运行 iOS 5.1 的 iPhone 4 上,这可能需要 250-350 毫秒。

在这三分之一秒的时间里,应用程序完全没有响应。保存之前正在进行的动画将暂停,并且在保存完成之前不会将新的用户事件发送到主运行循环。

我使用 Time Profiler 在 Instruments 中运行我们的应用程序,以确定是什么阻塞了我们的主线程。不幸的是,结果相当不透明。这是我从 Instruments 获得的最重的堆栈跟踪。

Instruments 最重的堆栈跟踪

似乎正在将更新保存到持久存储,但我不能确定。所以我完全删除了所有调用,saveContext这样 MOC 就不会接触磁盘,并且主线程上的阻塞调用仍然存在。

文本形式的跟踪如下所示:

Symbol Name
-[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
 -[NSManagedObjectContext executeFetchRequest:error:]
  -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]
   _perform
    _dispatch_barrier_sync_f_invoke
     _dispatch_client_callout
      __82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke_0
       -[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
        -[NSManagedObjectContext executeFetchRequest:error:]
         -[NSPersistentStoreCoordinator executeRequest:withContext:error:]
          -[NSSQLCore executeRequest:withContext:error:]
           -[NSSQLCore objectsForFetchRequest:inContext:]
            -[NSSQLCore newRowsForFetchPlan:]
             -[NSSQLCore _newRowsForFetchPlan:selectedBy:withArgument:]
              -[NSSQLiteConnection execute]

我试过的

在我们接触 Core Data 代码之前,我们做的第一件事就是优化我们的 JPEG。我们切换到更小的 JPEG 并看到了性能提升。然后,我们减少了一次下载的 JPEG 数量(从 90 个减少到 15 个)。这也导致显着的性能提升。但是,我们仍然在主线程上看到 250-350 毫秒长的块。

我尝试的第一件事就是摆脱后台 MOC 以消除它可能导致问题的可能性。事实上,这让事情变得更糟,因为我们的更新或创建代码在主线程上运行并导致整体动画性能下降。

将持久存储更改为NSInMemoryStoreType无效。

谁能指出我的“秘诀”,它将为我提供后台托管对象上下文所承诺的 UI 性能?

4

2 回答 2

6

我会做一些假设,但从你的描述来看,我认为这是合理的。

首先,我假设您的主要 MOC 是通过以下方式创建的:

[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

我做出这个假设是因为...

  1. 您将其用作父上下文,因此它必须是NSMainQueueConcurrencyTypeor NSPrivateQueueConcurrencyType

  2. 由于您在其他地方使用它,因此只有在保证可以在主队列上访问它的情况下才会这样做。

现在,我还假设您已将主 MOC 直接连接到持久存储协调器,而不是连接到另一个父 MOC。

当后台 MOC 进行保存时,其更改会向上传播到主 MOC(尽管它们尚未保存)。由于您的 FRC 已附加到主 MOC,因此它将立即看到这些更改。

当您在主 MOC 上发出保存时,您的代码将阻塞,因为该保存在保存完成之前不会返回。

所以,你所看到的完全是意料之中的。

有很多选项可以解决您的问题。

我的第一个建议是创建一个私有队列 MOC 并使其成为主队列 MOC 的父级。

这意味着主队列 MOC 的任何保存都不会阻塞。相反,它会将数据“保存”到父级,然后父级将在其自己的私有队列中执行实际的数据库保存,结果是在后台的单独线程中。

现在,这将解决您的主线程阻塞问题。这种机制也很适合加载数据库的子背景 MOC。

请注意,iOS 5 中存在一些与嵌套上下文相关的错误,但如果您的目标是 iOS 6,则其中大部分都已修复。

有关详细信息,请参阅在获取永久 ID 后对象的核心数据无法完成错误

编辑

您的假设是正确的,尽管我担心我的目标是 iOS 5,并且只能在 iOS 6 上拥有一个主 MOC 的父 MOC(这会导致 FRC 陷入僵局)。– 灰沟

如果你看到死锁,首先摆脱你的dispatch_syncperformBlockAndWait电话。除了最简单的同步(即,从数据结构中同步读取操作)之外,绝不应该在主线程中使用阻塞操作……并且仅在必要时才发生。此外,同步调用可能会导致意外死锁,因此应尽可能避免,尤其是在调用您不直接控制的任何代码时。

如果你不能这样做,还有其他一些选择。我最喜欢的是将 FRC 附加到私有队列 MOC,然后通过主线程“粘合”您对 FRC 的访问/从 FRC 访问。您可以dispatch_async到主线程来处理对表视图(或其他)的委托更新。例如...

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    dispatch_async(dispatch_get_main_queue(), ^{
        [tableView beginUpdates];
    });
}

您可以为 FRC 创建一个代理,它只是一个确保访问适当同步的前端。它不必是一个完整的代理......足以满足您使用 FRC 的目的......并确保所有操作都适当同步。

它实际上比听起来容易。

实际上,您将拥有与现在完全相同的设置,除了“main-MOC”将是私有队列 MOC 而不是主队列 MOC。因此,当数据库访问发生时,您的主线程不会阻塞。

但是,您需要采取一些预防措施,以确保在正确的上下文中使用 FRC。

另一种选择是使用您的主 MOC 和 FRC,就像您现在所做的那样来处理对数据库的更改,但使数据库的所有修改都通过一个单独的 MOC,直接连接到持久存储协调器。这使得更改发生在单独的线程中。然后,您使用 MOC 保存通知来更新您的上下文和/或从存储中重新获取数据。

iOS 和 OSX 的更新修复了 Core Data 的嵌套上下文的许多问题,但是在支持以前的版本时,您需要担心一些事情。

于 2012-10-29T01:15:07.723 回答
0

我有一些猜测,按照你应该检查它们的顺序:

  1. 它看起来很像在保存数据后发生的任何缓慢的事情(即,它在回调中)。这导致我要么重新加载,要么PTSCollectionView重新获取获取的结果控制器。

  2. 这个问题UICollectionView在 iOS 6.x 上会发生吗?如果不是,那将导致我依靠PTSCollectionView.

  3. 如果它仍然发生,那么这意味着它可能不是集合视图,而是获取的结果控制器。看起来,从堆栈帧(尽管它们可能是不透明的)来看,获取的结果控制器正在尝试执行通过dispatch_barrier. 这些用于确保在达到障碍之前不会执行块。我在这里很困难,但您可能想检查一下,这是因为在内部,Core Data 正在保存在其他地方,因此延迟了任何其他获取请求的执行。再一次,这是一种疯狂且未受过教育的猜测。但我会尝试不立即更新获取的结果控制器,看看你的口吃是否仍然发生。

对我来说突出的另一件事是您正在对孩子执行大量工作,MOC然后对父母执行保存。似乎大部分保存应该由孩子执行。但也可能是我有一段时间没有使用过 Core Data 的这一部分了 :-)

于 2012-10-28T17:06:49.660 回答