25

编辑 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 来了,这个问题又回来了。幸运的我。以前我将问题缩小到在表视图上使用estimatedRowHeight(顺便说一句,我从来没有完全解决这个问题。我只是停止使用estimatedRowHeight)。现在我在不同的情况下再次看到这个问题。几天前,当我将导航/标签栏设置为半透明时,我将其追踪到了一次提交。这包括在情节提要中禁用“调整滚动视图插图”并选中复选框以使我的视图显示在顶栏和底栏下。为了实现它,我需要执行一系列步骤,但是我每次都可以通过以这种方式配置的情节提要来重现它。如果我恢复该提交,它将不再发生。

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

编辑5:

如果您想阅读我的全部痛苦,请继续阅读这篇文章。如果您遇到此问题并且您只是想要一些帮助,这里有一些东西需要调查。

我的最后一次编辑指出,当我使用基本的表格视图单元格时,一切正常。我的下一步行动是尝试从头开始逐个构建一个新的自定义单元,看看它在哪里搞砸了。为了它,我重新启用了我的旧自定义单元代码,它工作得很好。呃?哦,等等,我还是estimatedHeightForRowAtIndexPath注释掉了。当我删除这些评论并启用estimatedHeightForRowAtIndexPath时,它又变得很糟糕。有趣的。

我在 API 文档中查找了该方法,它提到了一些关于名为UITableViewAutomaticDimension. 我估计的值实际上只是常见的单元格高度之一,因此切换到该常数不会有什么坏处。切换到该常数后,它可以正常工作。没有奇怪的异常/图形故障需要报告。

原帖

我有一个非常标准的 iPhone 应用程序,它在后台从 Web 服务获取数据并在表格视图中显示数据。后台更新工作为 NSPrivateQueueConcurrencyType 配置了自己的托管对象上下文。我的表视图的获取结果控制器具有为 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

这是我看到的例外...

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 试图插入 nil 对象?),但我有更多的信息和案例是可重复的。我所有的合并事件都是插入。在合并期间,后台上下文中似乎没有任何待处理的插入、删除或更新。我最初在所有地方都使用 performBlockAndWait,直到我从 WWDC 视频中了解到 performBlock 和 performBlockAndWait 之间的区别。我切换到 performBlock,这让它变得更好了一点。最初,我将其视为线程问题,后来发现它可能是由于不完全理解块而导致的奇怪内存问题,现在我又回到了竞争条件。好像只有一件我错过了。有两种方法不会发生...

(1)注册上下文将保存通知,当我得到它时将 FRC 委托归零,并在合并后将委托设置回来。这与根本不使用 FRC 相差无几,因此这确实不是一种解决方法。

(2) 做一些阻塞主线程足够长的事情,这样竞争条件就不会发生。例如,当我向我的表视图委托添加大量调试日志消息时,这会减慢它的速度,以至于它不会发生。

以下是我认为重要的代码片段(我已经缩短了某些位置以缩小这个已经很大的帖子)。

在滚动期间的各个点之后,视图控制器将通过调用其中包含此的函数来请求更多数据......

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

保存后,将触发合并通知。我只是从后台合并到主要,所以我只是想知道我是否可以内联合并调用,或者我是否需要在 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];
}

另一段可能感兴趣的代码。我正在为我的表格视图单元格使用自动布局,并为动态单元格高度使用新的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 行更新表,所以我只包含调试输出中有趣的部分。每次配置单元格时,我都会打印出刚刚出列的单元格的标题以及新标题的内容。当我点击表格视图中的最后一个单元格时,我会向 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 吗?

我获取的结果控制器正在使用按日期和开始时间排序的排序描述符。也许在 r17 和 r22 的现有事件没有收到移动事件,因为没有任何与它们的 NSManagedObjects 相关的更改。本质上,它们需要移动是因为我对比它们更早的事件的排序描述符,而不是因为它们的数据发生了变化。

编辑2:

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

编辑3:

我今天尝试的事情...

  1. 使 AFNetworking 成功块在返回之前等待合并完成
  2. 如果获取的结果控制器位于 beginUpdates/endUpdates 的中间,则使 cellForRowAtIndexPath 返回一个陈旧的单元格(基本上将其出列并立即返回)。认为在更新期间调用的额外随机 cellForRowAtIndexPath 可能一直在做奇怪的事情。
  3. 完全删除背景上下文。这是有趣的。如果我在主上下文中进行所有 UI 更新和 JSON 解析,它仍然会发生。

编辑4:

现在它变得有趣了。

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

那行得通。

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

那行得通。

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

那行得通。

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

4

1 回答 1

4

来自 Apple 技术支持工程师:

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

多线程错误是神秘 Core Data 问题的最常见原因。

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

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

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

CoreData 并发调试

Ole Begemann对新功能有很好的描述

于 2014-09-04T19:00:08.837 回答