后台托管对象上下文交错UI动画

Ash*_*row 12 user-interface core-data objective-c nsmanagedobjectcontext ios

我有一个问题,我已经工作了几个星期.每当我保存我的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商店.最后,向委托进行回调,通知他们响应已完全插入到Core Data存储中.

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

[AppDelegate.managedObjectContext performBlock:^{
    [AppDelegate saveContext]; //A standard save: call to the main MOC
}];
Run Code Online (Sandbox Code Playgroud)

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

问题

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

在三分之一秒内,该应用程序完全没有响应.暂停之前正在进行的动画暂停,并且在保存完成之前,没有新的用户事件发送到主运行循环.

我使用Time Profiler在Instruments中运行我们的app,以确定阻塞主线程的内容.不幸的是,结果相当不透明.这是我从仪器获得的最重的堆栈跟踪.

仪器最重的堆栈跟踪

似乎是保存持久存储的更新,但我无法确定.所以我完全删除了任何调用,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]
Run Code Online (Sandbox Code Playgroud)

我曾经尝试过什么

在我们触及核心数据代码之前,我们做的第一件事是优化我们的JPEG.我们切换到较小的JPEG并看到性能提升.然后,我们减少了我们一次下载的JPEG数量(从90减少到15).这也可以显着提升性能.但是,我们仍然在主线程上看到250-350ms长的块.

我尝试的第一件事就是摆脱背景MOC以消除它可能导致问题的可能性.实际上,由于我们的更新或创建代码在主线程上运行并导致整体动画性能降低,因此它使事情变得更糟.

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

任何人都可以指向我的"秘密酱",它将为我提供后台管理对象上下文所承诺的UI性能吗?

Jod*_*ins 6

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

首先,我假设您的主要MOC已创建:

[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
Run Code Online (Sandbox Code Playgroud)

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

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

  2. 由于您在其他地方使用它,因此只有在保证在主队列中访问它时才会这样做.

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

当后台MOC进行保存时,其更改会传播到主MOC中(尽管它们尚未保存).由于您的FRC连接到主MOC,它将立即看到这些更改.

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

所以,你所看到的是完全可以预期的.

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

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

这意味着主队列MOC的任何保存都不会阻止.相反,它会将数据"保存"到父级,然后父级将在其自己的专用队列中进行实际的数据库保存,从而在后台的单独线程中进行保存.

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

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

有关更多信息,请参阅获取PermanantID之后的Core Data无法为对象填充故障.

编辑

你的假设是正确的,虽然我恐怕我的目标是iOS 5并且只能在iOS 6上拥有主MOC的父MOC(它会导致FRC陷入僵局). - Ash Furrow

如果你看到死锁,首先摆脱你dispatch_syncperformBlockAndWait电话.除了最简单的同步(即,从数据结构同步读取操作)之外的任何事情都不应该在主线程内使用阻塞操作......然后仅在必要时使用.此外,同步调用可能会导致意外的死锁,因此应尽可能避免,特别是在调用任何不直接控制的代码时.

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

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

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

它实际上比听起来更容易.

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

但是,您需要采取一些预防措施,以确保在适当的环境中使用FRC.

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

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


jbr*_*nan 0

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

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

  2. iOS 6.x 上是否会出现此问题UICollectionView?如果没有,那会让我依靠PTSCollectionView

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

另一件对我来说很突出的事情是,您对孩子执行了大量工作MOC,然后对父母执行保存。看起来大部分保存工作应该由孩子来执行。但也可能是我有一段时间没有使用核心数据的这一部分了:-)