使用GCD,块和核心数据冻结(而不是崩溃)

时间:2012-05-16 08:23:28

标签: iphone core-data grand-central-dispatch freeze

我最近重写了我的Core Data驱动数据库控制器,以使用Grand Central Dispatch来管理后台的提取和导入。控制器可以在2个NSManagedContext上运行:

  1. NSManagedObjectContext *mainMoc主线程的实例变量。此上下文仅供主线程或dipatch_get_main_queue()全局队列快速访问UI使用。

  2. NSManagedObjectContext *bgMoc用于后台任务(为表格导入和获取NSFetchedresultsController的数据)。此后台任务仅由用户定义的队列触发:dispatch_queue_t bgQueue(数据库控制器对象中的实例变量)。

  3. 获取表的数据是在后台完成的,以便在执行更大或更复杂的谓词时不阻止用户UI。

    在我的表视图控制器中获取NSFetchedResultsController的示例:

    -(void)fetchData{
    
    dispatch_async([CDdb db].bgQueue, ^{
    
            NSError *error = nil;
            [[self.fetchedResultsController fetchRequest] setPredicate:self.predicate];
            if (self.fetchedResultsController && ![self.fetchedResultsController performFetch:&error]) {
    
                NSSLog(@"Unresolved error in fetchData %@", error);
            }
    
            if (!initial_fetch_attampted)initial_fetch_attampted = YES;
            fetching = NO;
    
            dispatch_async(dispatch_get_main_queue(), ^{
    
                [self.table reloadData];
                [self.table scrollRectToVisible:CGRectMake(0, 0, 100, 20) animated:YES];
            });
    
        });
    

    } // fetchData函数结束

    bgMoc使用mainMoc

    与保存时NSManagedObjectContextDidSaveNotification合并
    - (void)bgMocDidSave:(NSNotification *)saveNotification {
    
        // CDdb - bgMoc didsave - merging changes with main mainMoc
        dispatch_async(dispatch_get_main_queue(), ^{
    
        [self.mainMoc mergeChangesFromContextDidSaveNotification:saveNotification];
         // Extra notification for some other, potentially interested clients
           [[NSNotificationCenter defaultCenter] postNotificationName:DATABASE_SAVED_WITH_CHANGES object:saveNotification];
    
        });
    }
    
    - (void)mainMocDidSave:(NSNotification *)saveNotification {
    
        // CDdb - main mainMoc didSave - merging changes with bgMoc
        dispatch_async(self.bgQueue, ^{
         [self.bgMoc mergeChangesFromContextDidSaveNotification:saveNotification];
         });
    }
    

    NSfetchedResultsController委托只实现了一个方法(为简单起见):

    - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    
        dispatch_async(dispatch_get_main_queue(), ^{
    
            [self fetchData];
    
        });
    
    }
    

    这样我就会尝试遵循Apple对Core Data的建议:每个线程1个NSManagedObjectContext。我知道这种模式并不完全清理,原因有两个:

    1. bgQueue在暂停后不一定会触发同一个线程,但因为它是串行的,所以它应该没什么关系(从来没有2个线程尝试访问bgMoc NSManagedObjectContext专用于它。)
    2. 有时候,表视图数据源方法会向NSFetchedResultsController询问bgMoc中的信息(因为在bgQueue上进行了提取),比如节数,节数中提取的对象等等。
    3. 有这个缺陷的事件,这个方法在95%的应用程序运行时间上运行良好,直到......

      和我的问题:

      有时,非常随机的应用程序冻结但不会崩溃。它不会对任何触摸做出响应,并且使其恢复生存的唯一方法是完全重新启动它(切换回背景并没有帮助)。 抛出没有异常,并且没有任何内容打印到控制台(我在Xcode中为所有异常设置了Breakpoints)。

      我曾尝试使用Instruments(特别是时间配置文件)对其进行调试,看看主线程上是否有什么问题,但没有任何内容出现。

      我知道GCD和核心数据是这里的主要嫌疑人,但我不知道如何跟踪/调试它。

      让我指出,当我仅异步地将所有任务分派到队列时(在所有地方使用 dispatch_async ),也会发生这种情况。这让我觉得它不仅仅是标准的死锁。

      是否有任何可能或提示我如何获得更多信息?一些额外的调试标志,乐器魔法技巧或构建设置等......

      非常感谢关于可能是什么原因的任何建议,以及(或)指向如何以更好的方式实现NSFetchedResultsController和后台导入的后台获取的指针。

2 个答案:

答案 0 :(得分:3)

我的第一个也是非常糟糕的错误是在后台队列中获取NSFetchedResultsController的数据。

经过测试后,我对抓取时间过于敏感了。我不必要将fetchData执行放回到后台线程,使得核心数据相关代码过于复杂,因为我可以生成的最长获取时间实际上是一秒钟的分裂。这引入了太多的复杂性和不确定性,以获得非常小的性能增益(如果有的话)。

我通过将fetchData执行和所有NSFetchedResultsControllerDelegate方法移动到主线程(通过删除GCD代码简化代码)来辞职。

完成此操作后,我不再需要mainMocDidSave:并且在注册主线程上下文的NSManagedObjectContextDidSaveNotification时取消注册。 我还可以删除和取消注册DATABASE_SAVED_WITH_CHANGES通知发布。

这种大大简化的“合并”机制从此时起仅在后台线程上下文中将其更改与主线程上下文(保存时)合并。我们称之为一个方向性变化通知。

NSFetchedResultsControllerDelegate方法将在合并后拾取主线程上下文更改时自动触发。

另一个重要的事情是将dispatch_async更改为 dispatch_sync

- (void)bgMocDidSave:(NSNotification *)saveNotification {

    // CDdb - bgMoc didsave - merging changes with main mainMoc
        // Previously was: dispatch_async
        // dispatch_sync in this place may prevent from overlapping merging in some cases (many blocks in background queue)
    dispatch_sync(dispatch_get_main_queue(), ^{

    [self.mainMoc mergeChangesFromContextDidSaveNotification:saveNotification];
     // !!! Extra notification NO needed anymore

    });
}

经验法则:SIMPLIFY和MINIMIZE线程数量和NSManagedContexts。 我经历过即使对于非常大的应用程序也有2个上下文

  1. importContext专用于GCD队列(必须是串行队列)。只需记住将它保存在队列块代码的末尾。
  2. mainConstext在主UI线程上运行(我称之为READ上下文;-)在为UI(表示)提取数据时使用。

答案 1 :(得分:1)

DATABASE_SAVED_WITH_CHANGES通知看起来有点可疑:假设bgMoc保存。然后bgMocDidSave:触发并将更改与mainMoc合并,这很好。然后你发出一个通知,最后(我假设{DATABASE_SAVED_WITH_CHANGES被引发时会发出mainMocDidSave:})将更改合并到bgMoc(这是源于哪里!)。这对我来说听起来不是正确的做法。 您也可以在bgMocDidSave:签入该通知来自bgMoc。如果mainMoc保存,那么bgMocDidSave:也会触发更改。