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

时间:2012-10-28 15:28:58

标签: objective-c ios core-data user-interface nsmanagedobjectcontext

我有一个问题,我已经工作了几个星期。每当我保存我的Core Data托管对象上下文时,它都会导致UI性能出现问题。我已经尽我所能并且正在寻找一些帮助。

情况

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

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

  1. 构造一个NSURLRequest对象并使用NSURLConnectionDataDelegate协议构造从请求返回的数据,或处理错误。
  2. 完成JSON数据的下载后,在辅助MOC的队列上执行以下操作,执行以下操作:
    1. 使用NSJSONSerialization将JSON解析为Foundation类实例。
    2. 根据需要迭代已解析的数据,在我的后台上下文中插入或更新实体。通常,这会产生大约300个新的或更新的实体。
    3. 保存背景上下文。这会将我的更改传播到主MOC。
    4. 在主MOC上执行一个块以保存它的上下文。这是为了将我们的数据保存到磁盘,SQLite商店。最后,向委托进行回调,通知他们响应已完全插入到Core Data存储中。
  3. 保存背景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-350ms之间。

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

    我使用Time Profiler在Instruments中运行我们的应用程序来识别阻塞我们主线程的内容。不幸的是,结果相当不透明。这是我从仪器获得的最重的堆栈跟踪。

    Instruments Heaviest Stack Trace

    出现是为了保存对持久性商店的更新,但我无法确定。所以我完全删除了对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]
    

    我尝试过什么

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

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

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

    有人能指出我的“秘密酱”,它会给我背景管理对象背景所承诺的UI性能吗?

2 个答案:

答案 0 :(得分:6)

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

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

[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

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

  1. 您将其用作父语境,因此必须为NSMainQueueConcurrencyTypeNSPrivateQueueConcurrencyType

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

  3. 现在,我还假设您已将主MOC直接连接到持久性商店协调员,而不是另一个父MOC。

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

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

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

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

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

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

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

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

    有关详细信息,请参阅Core Data could not fullfil fault for object after obtainPermanantIDs

    修改

      

    你的假设是正确的,但我担心我的目标是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];
        });
    }
    

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

    它实际上比听起来容易。

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

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

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

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

答案 1 :(得分:0)

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

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

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

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

  4. 另一件让我感到惊讶的事情是,您正在对孩子MOC执行大量工作,然后对父母执行保存。似乎大部分保存应该由孩子执行。但也可能是我在一段时间内没有使用过Core Data的这一部分: - )