ios - NSFetchedResultsController尝试插入nil对象

标签 ios objective-c uitableview core-data nsfetchedresultscontroller

编辑7:

这是我的保存方法。这是很漂亮的样板。仅在调试版本时才执行DEBUG_LOG()宏。

- (void)saveManagedObjectContext:(NSManagedObjectContext *)moc
{
    if ([moc hasChanges]) {
        DEBUG_LOG(@"Saving managed object context %@", moc);
        NSError *error;
        BOOL success = [moc save:&error];
        if (!success || error) {
            DEBUG_LOG(@"ERROR: Couldn't save to managed object context %@: %@",
                  moc, error.localizedDescription);
        }
        DEBUG_LOG(@"Finished saving managed object context %@", moc);
    } else {
        DEBUG_LOG(@"Managed object context %@ had no changes", moc);
    }
}

编辑6:

iOS 8在这里,这个问题又回来了。幸运的我。以前,我将问题缩小为在表 View 上使用EstimatedRowHeight(顺便说一句,我从未完全解决问题。我只是停止使用EstimatedRowHeight)。现在,我在不同的情况下又遇到了这个问题。我将其导航到几天前的一次提交,当时我将导航/制表符栏设为半透明。这包括在 Storyboard 中禁用“调整滚动 View 插图”,并选中相应的框以使我的 View 显示在顶部和底部。我需要执行一系列步骤来实现它,但是每次以这种方式配置的 Storyboard时,我都可以重现它。如果我还原该提交,它将不再发生。

当我说“它不再发生”时,我真正想到的是,这只会使其发生的可能性降低。这个错误是绝对的b ****。我现在的直觉是这是一个iOS错误。我只是不知道该怎么做才能将其转换为错误报告。疯了

编辑5:

如果您想阅读我的所有苦难,请继续阅读本帖子。如果您遇到此问题,并且只需要一些帮助,可以引用以下内容。

我的上一次编辑指出,当我使用基本的表格 View 单元格时,一切正常。我的下一个行动方针是尝试从头开始逐个构建一个新的自定义单元,并查看它的困惑之处。对于它的 hell ,我重新启用了旧的自定义单元代码,它工作得很好。嗯?哦,等等,我还有estimatedHeightForRowAtIndexPath被注释掉了。当我删除这些注释并启用estimatedHeightForRowAtIndexPath时,它又变得糟透了。有趣的。

我在API文档中查找了该方法,其中提到了有关名为UITableViewAutomaticDimension的常量的内容。我估计的值实际上只是常见单元格高度之一,因此切换到该常数不会有任何伤害。切换到该常数后,它可以正常工作。没有奇怪的异常(exception)/图形故障要报告。

原始帖子

我有一个非常标准的iPhone应用程序,该应用程序在后台从Web服务中获取数据并在表格 View 中显示数据。后台更新工作具有为NSPrivateQueueConcurrencyType配置的自己的托管对象上下文。我的表 View 的访存结果 Controller 具有为NSMainQueueConcurrencyType配置的自己的托管对象上下文。当后台上下文解析新数据时,它将通过mergeChangesFromContextDidSaveNotification将该数据传递到主上下文。有时在合并期间,我的应用程序在这里遇到异常...
Thread 1, Queue : com.apple.main-thread
#0  0x3ac1b6a0 in objc_exception_throw ()
#1  0x308575ac in -[__NSArrayM insertObject:atIndex:] ()
#2  0x33354306 in __46-[UITableView _updateWithItems:updateSupport:]_block_invoke687 ()
#3  0x330d88d2 in +[UIView(UIViewAnimationWithBlocks)     _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] ()
#4  0x330ef7e4 in +[UIView(UIViewAnimationWithBlocks) animateWithDuration:delay:options:animations:completion:] ()
#5  0x3329e908 in -[UITableView _updateWithItems:updateSupport:] ()
#6  0x332766c6 in -[UITableView _endCellAnimationsWithContext:] ()
#7  0x0005ae72 in -[ICLocalShowsTableViewController controllerDidChangeContent:] at ICLocalShowsTableViewController.m:475
#8  0x3069976c in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:] ()
#9  0x308dfe78 in __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ ()
#10 0x30853b80 in _CFXNotificationPost ()
#11 0x3123a054 in -[NSNotificationCenter postNotificationName:object:userInfo:] ()
#12 0x306987a2 in -[NSManagedObjectContext(_NSInternalNotificationHandling) _postObjectsDidChangeNotificationWithUserInfo:] ()
#13 0x306f952a in -[NSManagedObjectContext _mergeChangesFromDidSaveDictionary:usingObjectIDs:] ()
#14 0x306f9734 in -[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:] ()
#15 0x0006b5be in __65-[ICManagedObjectContexts backgroundManagedObjectContextDidSave:]_block_invoke at ICManagedObjectContexts.m:133
#16 0x306f9854 in developerSubmittedBlockToNSManagedObjectContextPerform ()
#17 0x3b1000ee in _dispatch_client_callout ()
#18 0x3b1029a8 in _dispatch_main_queue_callback_4CF ()
#19 0x308e85b8 in __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ ()
#20 0x308e6e84 in __CFRunLoopRun ()
#21 0x30851540 in CFRunLoopRunSpecific ()
#22 0x30851322 in CFRunLoopRunInMode ()
#23 0x355812ea in GSEventRunModal ()
#24 0x331081e4 in UIApplicationMain ()
#25 0x000554f4 in main at main.m:16

这是我看到的异常(exception)情况...
CoreData: error: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  *** -[__NSArrayM insertObject:atIndex:]: object cannot be nil with userInfo (null)

我的应用程序实际上在我对endUpdates的调用中遇到了controllerDidChangeContent中的异常。我基本上看到的是与此(NSFetchedResultsController attempting to insert nil object?)相同的东西,但是我有更多信息,而且情况可重复。我所有的合并事件都是插入。在合并期间,在后台上下文中似乎没有任何挂起的插入,删除或更新。最初,我在各处都使用了performBlockAndWait,直到从WWDC视频了解了performBlock和performBlockAndWait之间的区别。我切换到performBlock,这使其变得更好一点。最初,我将其作为线程问题来解决,然后又归因于由于无法完全理解块而导致的奇怪的内存问题,现在我又回到了竞争状态。似乎我只缺少一件。不会发生两种情况...

(1)注册上下文将保存通知,在收到FRC委托(delegate)时将其取消,并在合并后重新设置该委托(delegate)。根本不使用FRC并不遥不可及,因此这实际上不是解决方法的选择。

(2)做一些阻塞主线程的时间足够长的操作,这样就不会发生竞争状况。例如,当我向表 View 委托(delegate)中添加许多调试日志消息时,这会使它减慢速度,以至于它不会发生。

我认为这是重要的代码段(为了缩短本已很大的职位,我缩短了某些位置)。

在滚动过程中的各个点之后, View Controller 将通过调用其中包含此功能的函数来请求更多数据。
AFJSONRequestOperation *operation =
    [AFJSONRequestOperation JSONRequestOperationWithRequest:request
        success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
            // Parsing happens on MOC background queue
            [backgroundMOC performBlock:^ {
                [self parseJSON:JSON];

                // Handle everything else on the main thread
                [mainMOC performBlock:^ {
                    if (completion) {
                        // Remove activitiy indicators and such from the main thread
                    }
                }];
            }];
        }

        failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
            [[NSOperationQueue mainQueue] performBlock:^ {
                if (completion) {
                    // Remove activitiy indicators and such from the main thread
                }
                // Show an alert view saying that the request failed
            }];
        }
     ];

[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
    return nil;
}];

[_operationQueue addOperation:operation];

在大多数情况下,parseJSON确实没有任何有趣的东西……
- (void)parseJSON:(NSDictionary *)json
{       
    NSError *error;
    NSArray *idExistsResults;
    NSNumber *eventId;
    NSFetchRequest *idExistsFetchRequest;
    LastFMEvent *event;
    NSManagedObjectModel *model = backgroundMOC.persistentStoreCoordinator.managedObjectModel;
    for (NSDictionary *jsonEvent in jsonEvents) {
        eventId = [NSNumber numberWithInt:[jsonEvent[@"id"] intValue]];
        idExistsFetchRequest = [model fetchRequestFromTemplateWithName:kGetEventByIDFetchRequest substitutionVariables:@{@"eventID" : eventId}];
        idExistsResults  = [backgroundMOC executeFetchRequest:idExistsFetchRequest error:&error];
        // Here I check for errors - omitted that part

        if ([idExistsResults count] == 0) {
            // Add a new event
            event = [NSEntityDescription insertNewObjectForEntityForName:[LastFMEvent entityName] inManagedObjectContext:backgroundMOC];
            [event populateWithJSON:jsonEvent];
        } else if ([idExistsResults count] == 1) {
            // Get here if I knew about the event already, so I update a few fields
        }
    }
    [self.mocManager saveManagedObjectContext:backgroundMOC];
}

保存和合并的实现可能会变得有趣。 Save期望已经从适当的performBlock内部被调用,因此对performBlock不会执行任何操作。
- (void)saveManagedObjectContext:(NSManagedObjectContext *)moc
{
    if ([moc hasChanges]) {
        NSError *error;
        BOOL success = [moc save:&error];
        if (!success || error) {
            NSLog(@"ERROR: Couldn't save to managed object context %@: %@",
                  moc, error.localizedDescription);
        }
    }
}

保存后,将触发合并通知。我只是从后台合并到主要,所以我几乎只想知道我是否可以内联merge调用,或者是否需要在performBlock内部进行。
- (void)backgroundManagedObjectContextDidSave:(NSNotification *)notification
{
    if (![NSThread isMainThread]) {
        [mainMOC performBlock:^ {
            [self.mainMOC mergeChangesFromContextDidSaveNotification:notification];
        }];
    } else {
        [mainMOC mergeChangesFromContextDidSaveNotification:notification];
    }
}

我获取的结果 Controller 委托(delegate)方法非常漂亮。
- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch (type) {

        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath]
                             withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:(ICLocalShowsTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]
                          atIndexPath:indexPath];
            break;
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationAutomatic];
            [tableView insertRowsAtIndexPaths:@[newIndexPath]
                             withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id )sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {

        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                          withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                          withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
}

可能感兴趣的另一段代码。我正在为表格 View 单元格使用自动布局,并为动态单元格高度使用了新的EstimatedHeightForRowAtIndexPath API。这意味着在调用[self.tableView endUpdates]的过程中,最后一步实际上到达了一些托管对象,而其他对节数/行数的调用只需要知道FRC的计数即可。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSAssert([NSThread isMainThread], @"");
    LastFMEvent *event = [self.fetchedResultsController objectAtIndexPath:indexPath];

    if (!_offscreenLayoutCell) {
        _offscreenLayoutCell = [self.tableView dequeueReusableCellWithIdentifier:kLocalShowsCellIdentifier];
    }

    [_offscreenLayoutCell configureWithLastFMEvent:event];
    [_offscreenLayoutCell setNeedsLayout];
    [_offscreenLayoutCell layoutIfNeeded];

    CGSize cellSize = [_offscreenLayoutCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    return cellSize.height;
}

现在已经坚持了将近一个星期。在此过程中学到了很多东西,但是,我准备继续前进。任何建议将不胜感激。

编辑

我整理了一个相当大的调试日志,以尝试讲述udpates发生了什么。我看到了一些很奇怪的东西。我一次要更新50张表,所以我只会在调试输出中包含有趣的部分。每次配置单元时,我都会打印出刚出队的单元的标题以及新标题。当我点击表 View 中的最后一个单元格时,我向Web服务查询了更多数据。此输出与我遇到异常之前的最终更新有关...
// Lots of output was here that I omitted

configure cell at sect 5 row 18 WAS Suphala NOW Keller Williams
configure cell at sect 5 row 19 WAS Advocate Of Wordz NOW Gates
configure cell at sect 5 row 20 WAS Emanuel and the Fear NOW Beats Antique
configure cell at sect 5 row 21 WAS The Julie Ruin NOW Ashrae Fax

// At this point I hit the end of the table and query for more data - for some reason row 18 gets configured again. Possibly no big deal.

configure cell at sect 5 row 18 WAS Keller Williams NOW Keller Williams
configure cell at sect 5 row 22 WAS Old Wounds NOW Kurt Vile

JSON size 100479
Starting JSON parsing
page 3 of 15. total events 709. events per page 50. current low idx 100 next trigger idx 149

// Parsing data finished, saving background context

Saving managed object context <NSManagedObjectContext: 0x17e912f0>
Background context will save
Finished saving managed object context <NSManagedObjectContext: 0x17e912f0>
Merging background context into main context
JSON parsing finished

** controllerWillChangeContent called **
** BEGIN UPDATES triggered **

inserting SECTION 6
inserting SECTION 7
inserting SECTION 8
inserting ROW sect 5 row 17
inserting ROW sect 5 row 22
inserting ROW sect 5 row 25
inserting ROW sect 5 row 26
inserting ROW sect 5 row 27
inserting ROW sect 5 row 28
inserting ROW sect 5 row 29

// A bunch more rows added here that I omitted

** controllerDidChangeContent called **

// This configure cell happens before the endUpdates call has completed

configure cell at sect 5 row 18 WAS Conflict NOW Conflict

在最后的更新中,它试图在s5 r17处插入,但是我已经在该行中有一个单元格了。它还尝试在s5 r22处插入,但是我在该行也已经有一个单元格。最后,它在s5 r25插入一行,实际上是新行。在我看来,考虑将r17和r22视为插入在表格中留下了空白。这些索引处的先前单元格是否不应将事件移至r23和r24?

我的提取结果 Controller 正在使用按日期和开始时间排序的排序描述符。也许在r17和r22处的现有事件没有发生移动事件,因为没有任何与其NSManagedObjects相关的更改。本质上,因为我的排序描述符早于它们的事件才需要移动它们,而不是因为它们的数据已更改。

编辑2:

看起来那些插入确实会触发现有单元格向下移动:(

编辑3:

我今天尝试过的东西...
  • Made AFNetworking成功块等待合并完成,然后返回
  • 如果获取的结果 Controller 位于beginUpdates/endUpdates的中间,则使cellForRowAtIndexPath返回一个陈旧的单元格(实质上是将其从队列中取出并立即返回)。认为在更新过程中调用了额外的随机cellForRowAtIndexPath可能正在做怪异的事情。
  • 完全删除背景上下文。这是有趣的。如果我在主上下文上执行所有UI更新和JSON解析,那么它仍然会发生。

  • 编辑4:

    现在变得有趣了。

    我尝试删除表格 View 中的随机组件,例如刷新控件。还尝试摆脱我对estimatedHeightForRowAtIndexPath的使用,这意味着仅提供静态行高,而不是使用自动布局来确定动态行高。那些都没发现。我还尝试完全摆脱我的自定义单元,而仅使用基本的表格 View 单元。

    那行得通。

    我尝试了带字幕的基本表格 View 单元格。

    那行得通。

    我尝试了带有字幕和图像的基本表格 View 单元格。

    那行得通。

    我的堆栈跟踪的顶部靠近所有那些与动画相关的项目,这开始变得更加有意义。看起来这与自动布局有关。

    最佳答案

    来自Apple技术支持工程师:

    To protect the integrity of the datastore, Core Data catches some exceptions that happen during its operations. Sometimes this means that if Core Data calls your code through a delegate method, Core Data may end up catching exceptions your code threw.

    Multi-threading errors are the most common cause of mysterious Core Data issues.



    在这种情况下,Core Data通过您的controllerDidChangeContent:方法捕获了一个异常,该异常是由尝试使用insertObject:atIndex引起的。

    最可能的解决方法是确保所有NSManagedObject代码都封装在performBlock:performBlockAndWait:调用中。

    在iOS 8和OSX Yosemite中,Core Data获得了检测并报告其并发模型违规的功能。每当您的应用访问错误的调度队列中的托管对象上下文或托管对象时,它都会引发异常。您可以通过Xcode的Scheme Editor在命令行上将-com.apple.CoreData.ConcurrencyDebug 1传递给您的应用来启用断言。

    奥莱·贝格曼(Ole Begemann)有一个great writeup of the new feature

    关于ios - NSFetchedResultsController尝试插入nil对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19462645/

    相关文章:

    ios - 来自字典的 JSON 编码 [无法调用 'dataWithJSONObject ...]

    ios - 如何将导航栏标题更改为图像?其他方法没有奏效

    ios - 为什么我不能调用另一个类的方法

    ios - UiTableView Header - 动态按钮事件处理程序

    ios - Xcode如何将目标打包为.framework(而不是.a库)

    ios - SKAction.removeFromParent 不要用 SKAction.repeatActionForever 删除 Sprite

    ios - 将 Core Data/SQLite 数据库下载到应用程序中?

    objective-c - Obj-C/ cocoa : Way to store temporary data during runtime

    ios - 快速在单元格问题中动态创建表格 View 按钮

    iphone - 无法弄清楚为什么我的应用程序在使用 NSKeyedArchivers/NSKeyedUnarchivers 时崩溃