试图插入nil对象的NSFetchedResultsController

编辑7:

这是我的保存方法。 这是漂亮的样板。 DEBUG_LOG()macros只在debugging版本时才被执行。

- (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在这里,这个问题又回来了。 幸运的我。 以前我已经把问题缩小到在表格视图上使用estimatedRowHeight(顺便说一下,我从来没有完全解决这个问题,我刚刚停止使用estimatedRowHeight)。 现在我在不同的情况下再次看到这个问题。 我跟踪了几天前的一个提交,当我的导航栏/标签栏变成半透明的时候。 这包括禁用故事板中的“调整滚动视图插页”,并选中框以使我的视图显示在顶部条和底部条之下。 我需要做一系列的步骤才能实现,但是我可以每次以这种方式configuration我的故事板来重现它。 如果我恢复提交它不再发生。

虽然我说“这不再发生”,但我真的认为这只是使它不太可能发生。 这个bug是一个绝对的b ****。 我现在的直觉是,这是一个iOS的错误。 我只是不知道我能做些什么来把它变成一个错误报告。 这是疯狂的。

编辑5:

如果你想阅读我的痛苦,请继续阅读这篇文章。 如果你遇到这个问题,你只是想得到一些帮助,这里有一些东西要看。

我最后一次编辑指出,当我使用一个基本的表格视图单元格,一切工作正常。 我的下一步行动是从零开始尝试构build一个新的定制单元,并看看它在哪里混乱。 对于它的地狱,我重新启用了我的旧的自定义单元的代码,它工作得很好。 唔? 等等,我还是estimatedHeightForRowAtIndexPath HeightForRowAtIndexPath是注释掉了。 当我删除这些评论,并启用estimatedHeightForRowAtIndexPath HeightForRowAtIndexPath,它再次蹩脚。 有趣。

我在API文档中查找了这个方法,并提到了一个名为UITableViewAutomaticDimension的常量。 我估计的价值实际上只是常见的细胞高度之一,所以切换到这个常数不会有什么伤害。 切换到这个常量后,它正常工作。 没有奇怪的例外/graphics故障报告。

原帖

我有一个非常标准的iPhone应用程序,从后台的Web服务中获取数据,并在表格视图中显示数据。 后台更新工作为NSPrivateQueueConcurrencyTypeconfiguration了自己的托pipe对象上下文。 我的表视图的抓取结果控制器有自己的托pipe对象上下文为NSMainQueueConcurrencyTypeconfiguration。 当后台上下文parsing新数据时,通过mergeChangesFromContextDidSaveNotification将数据传递给主上下文。 有时在合并过程中,我的应用程序在这里遇到了一个exception…

 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 

这是我看到的例外…

 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) 

我的应用程序实际上触发了controllerDidChangeContent中的exception,在我对endUpdates的调用中。 我基本上看到这样的事情( NSFetchedResultsController试图插入零对象? ),但我有更多的信息和情况下,可重复。 我所有的合并事件都是插入的。 在合并过程中,似乎没有任何待处理的插入,删除或更新背景上下文。 我最初使用的是performBlockAndWait,直到我从WWDCvideo中了解到performBlock和performBlockAndWait之间的区别。 我切换到执行块,这使得它更好一点。 起初,我把这个问题看作是一个线程问题,分解成可能性是由于不完全理解块造成的一个奇怪的内存问题,现在我回到它是一个竞争条件。 似乎只有一件我错过了。 有两种方式不会发生…

(1)注册上下文将保存通知,当我得到它时将FRC委托清零,并在合并之后重新设置委托。 这不是根本不使用FRC,所以这不是一个解决scheme的选项。

(2)阻塞主线足够长的时间,所以竞赛条件不会发生。 例如,当我添加了很多debugging日志消息到我的表视图委托,这减缓了足够的不足以发生。

这里是我认为是重要的代码片段(我缩短了某些地方缩小了这个已经很大的post)。

在滚动过程中的各个点之后,视图控制器将通过调用具有此function的函数请求更多的数据。

 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]; } 

保存和合并的实现可能会变得有趣。 保存期望已经在相应的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); } } } 

保存后,合并通知被触发。 我只是从背景合并到main,所以我几乎只想知道是否可以内联合并调用,或者如果我需要在performBlock内执行。

 - (void)backgroundManagedObjectContextDidSave:(NSNotification *)notification { if (![NSThread isMainThread]) { [mainMOC performBlock:^ { [self.mainMOC mergeChangesFromContextDidSaveNotification:notification]; }]; } else { [mainMOC mergeChangesFromContextDidSaveNotification:notification]; } } 

我提取的结果控制器委托方法是漂亮的锅炉板材…

 - (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]; } 

另一个代码可能是有趣的。 我为我的表格视图单元格使用了自动布局,而dynamic单元格高度则使用了新的estimatedHeightForRowAtIndexPath API。 这意味着在调用[self.tableView endUpdates]的过程中,最后一步实际上到达一些被pipe理对象,而另一个调用段/行数只需要知道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; } 

一直持续了近一个星期。 在这个过程中学到了一吨,但是我准备继续前进。 任何build议将不胜感激。

编辑

我把一个相当大的debugging日志放在一起,试图讲述udpates发生了什么事情。 我看到一些很奇怪的东西 我一次更新50行,所以我只包含我的debugging输出中有趣的部分。 每当一个单元格被configuration,我打印出刚刚出列的单元格的标题以及新的标题。 当我点击表格视图中的最后一个单元格时,我向Web服务查询更多的数据。 此输出与最终更新有关,然后我遇到exception…

 // 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?

我提取的结果控制器正在使用按date和开始时间sorting的sorting描述符。 也许在r17和r22现有的事件没有得到移动事件,因为没有任何与NSManagedObjects相关的变化。 从本质上讲,它们需要移动,因为我的sorting描述符早于它们的事件,而不是因为它们的数据改变了。

编辑2:

看起来像插入只是触发现有的单元格下移:(

编辑3:

我今天试过的东西…

  1. 使AFNetworking成功块在返回之前等待合并完成
  2. 如果获取的结果控制器位于beginUpdates / endUpdates中间,则cellForRowAtIndexPath将返回一个陈旧的单元(实质上是将它退出并立即将其返回)。 认为在更新过程中被调用的额外的随机cellForRowAtIndexPath可能已经做了奇怪的事情。
  3. 完全删除后台上下文。 这很有趣。 如果我在主要上下文中执行UI更新和JSONparsing,它仍然会发生。

编辑4:

现在变得有趣了。

我尝试删除我的表视图中的随机组件,如刷新控制。 也试图摆脱我使用estimatedHeightForRowAtIndexPath,这意味着只是提供一个静态行高度,而不是使用自动布局来确定dynamic行高度。 这两个都没有出现。 我也尝试完全摆脱我的自定义单元格,并使用基本的表格视图单元格。

这工作。

我试了一个基本的表格视图单元格与副标题。

这工作。

我尝试了一个基本的表格视图单元格与字幕和图像。

这工作。

我的堆栈轨迹的顶部正在接近所有这些animation相关的项目开始变得更有意义。 这看起来像这是自动布局相关。

来自Apple技术支持工程师:

为了保护数据存储的完整性,核心数据捕获了操作过程中发生的一些exception。 有时这意味着如果核心数据通过委托方法调用你的代码,核心数据最终可能会捕获你的代码抛出的exception。

multithreading错误是神秘的核心数据问题最常见的原因。

在这种情况下,Core Data通过您的controllerDidChangeContent:方法捕获exception,这是由尝试使用insertObject:atIndex

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

在iOS 8和OSX Yosemite中,核心数据可以检测和报告违反其并发模型的能力。 它通过抛出一个exception,每当你的应用访问一个托pipe的对象上下文或从错误的调度队列pipe理的对象。 您可以通过在命令行上通过Xcode的Scheme Editor将-com.apple.CoreData.ConcurrencyDebug 1传递给您的应用程序来启用断言。

CoreData ConcurrencyDebug

Ole Begemann 对这个新function进行了很好的描述 。