NSFetchedResultsController尝试插入nil对象 [英] NSFetchedResultsController attempting to insert nil object

查看:209
本文介绍了NSFetchedResultsController尝试插入nil对象的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

编辑7:



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

   - (void)saveManagedObjectContext:(NSManagedObjectContext *)moc 
{
if([moc hasChanges]){
DEBUG_LOG(@保存管理对象上下文%@,moc);
NSError * error;
BOOL success = [moc save:& error];
if(!success || error){
DEBUG_LOG(@ERROR:无法保存到受管对象上下文%@:%@,
moc,error.localizedDescription);
}
DEBUG_LOG(@完成保存管理对象上下文%@,moc);
} else {
DEBUG_LOG(@Managed object context%@ has no changes,moc);
}
}

编辑6: / p>

iOS 8在这里,这个问题回来了。幸运的我。以前,我已经把问题缩小到使用estimatedRowHeight对表视图(btw,我从来没有完全解决这个问题,我只是停止使用estimatedRowHeight)。现在我在不同的情况下再次遇到这个问题。我跟踪它,从几天前,当我让我的导航/标签栏半透明的提交。这包括禁用故事板中的Adjust Scroll View Insets,并选中复选框以使我的视图显示在顶部条和底部条下。有一系列的步骤,我需要做,以使它发生,但我可以重现它每次与我的故事板配置这样的方式。如果我还原那个提交,它不会再发生了。



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



编辑5:



的我的苦难,请继续一路通过这个职位。如果你遇到这个问题,你只是想要一些帮助,这里有一些东西要研究。



我最后一次编辑时注意到,当我使用一个基本的表格视图单元格,一切都很好。我的下一步行动是从零开始尝试一个新的定制细胞,看看它在哪里弄乱了。对于它的地狱,我重新启用了我的旧的自定义单元格的代码,它的工作很好。呃?哦,等待,我还有 estimatedHeightForRowAtIndexPath 已注释掉。当我删除这些注释,并启用 estimatedHeightForRowAtIndexPath ,它再次。got。有趣。



我在API文档中查找了该方法,并提到了一个名为 UITableViewAutomaticDimension 的常量。我估计的值只是一个常见的细胞高度,所以它不会伤害切换到那个常数。切换到该常量后,它工作正常。没有奇怪的异常/图形故障报告。



原始帖子



一个漂亮的标准iPhone应用程序,它从后台的Web服务获取数据,并在表视图中显示数据。后台更新工作具有为NSPrivateQueueConcurrencyType配置的自己的管理对象上下文。我的表视图的获取结果控制器有为NSMainQueueConcurrencyType配置的自己的managed对象上下文。当后台上下文解析新数据时,它通过 mergeChangesFromContextDidSaveNotification 将该数据传递给主上下文。有时在合并期间,我的应用程序在此处遇到异常...

 线程1,队列:com.apple.main- 
#0 0x3ac1b6a0 in objc_exception_throw()
#1 0x308575ac in - [__ NSArrayM insertObject:atIndex:]()
#2 0x33354306 in __46- [UITableView _updateWithItems:updateSupport:] _ block_invoke687 b:
#3 0x330d88d2 in + [UIView(UIViewAnimationWithBlocks)_setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion: 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 - [NSManagedObjectContext(_NSInternalNotificationHandling)_postObjectsDidChangeNotificationWithUserInfo:]()
# 13 0x306f952a in - [NSManagedObjectContext _mergeChangesFromDidSaveDictionary:usingObjectIDs:]()
#14 0x306f9734 - [NSManagedObjectContext mergeChangesFromContextDidSaveNotification:]()
#15 0x0006b5be在__65- [ICManagedObjectContexts backgroundManagedObjectContextDidSave:] _ block_invoke at ICManagedObjectContexts.m: 133
#16 0x306f9854在developerSubmittedBlockToNSManagedObjectContextPerform()
#17 0x3b1000ee在_dispatch_client_callout()
#18 0x3b1029a8在_dispatch_main_queue_callback_4CF()
#19 0x308e85b8在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
# ()
#23 0x358512ea在CSEventRunModal()
#24 0x331081e4在UIApplicationMain()中的CFRunLoopRunSpecific()
#22 0x30851540在CFRunLoopRunInMode
#25 0x000554f4 in main.m:16

这是我看到的例外。 ..

  CoreData:错误:严重的应用程序错误。在对-controllerDidChangeContent:的调用期间,NSFetchedResultsController的委托捕获了异常。 ***  -  [__ NSArrayM insertObject:atIndex:]:object不能为nil with userInfo(null)

我的应用程序实际上击中了controllerDidChangeContent中的异常,在我调用endUpdates。我基本上看到了同样的事情(。 p>

Edit 7:

Here's my save method. It's pretty boilerplate. The DEBUG_LOG() macros are only executed if it's a debug build.

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

Edit 6:

iOS 8 is here and this problem is back. Lucky me. Previously I had narrowed the problem down to using estimatedRowHeight on table views (btw, I never fully fixed the problem. I just stopped using estimatedRowHeight). Now I'm seeing this problem again under different circumstances. I tracked it down to a commit from a few days ago when I made my nav/tab bars translucent. This included disabling 'Adjust Scroll View Insets' in storyboard and checking the boxes to have my views display under top bars and bottom bars. There's a series of steps I need to do to make it happen, but I can reproduce it every time with my storyboard configured that way. If I revert that commit it no longer happens.

While I say, "it no longer happens," what I really think is that this is just making it less likely to happen. This bug is an absolute b****. My gut reaction now is that this is an iOS bug. I just don't know what I can do to turn this into a bug report. It's madness.

Edit 5:

If you want to read the entirety of my misery, please continue all the way through this post. If you're running into this problem and you just want some help, here's something to look into.

My last edit noted that when I used a basic table view cell, everything worked fine. My next course of action was going to be to try from scratch building a new custom cell piece by piece and seeing where it messed up. For the hell of it, I re-enabled my old custom cell code and it worked just fine. Uhhh? Oh wait, I still have estimatedHeightForRowAtIndexPath commented out. When I removed those comments and enabled estimatedHeightForRowAtIndexPath, it got crappy again. Interesting.

I looked up that method in the API doc, and it mentioned something about a constant called UITableViewAutomaticDimension. The value I was estimating was really just one of the common cell heights, so it wouldn't hurt to switch to that constant. After switching to that constant it's working properly. No weird exceptions/graphical glitches to report.

Original post

I have a pretty standard iPhone app that fetches data from a web service in the background and displays data in a table view. The background updating work has its own managed object context configured for NSPrivateQueueConcurrencyType. My table view's fetched results controller has its own managed object context configured for NSMainQueueConcurrencyType. When the background context parses new data it passes that data to the main context via mergeChangesFromContextDidSaveNotification. Sometimes during the merge, my app hits an exception here...

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

Here's the exception I see...

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)

My app is actually hitting the exception in controllerDidChangeContent, at my call to endUpdates. I'm basically seeing the same thing as this (NSFetchedResultsController attempting to insert nil object?), but I've got more info and case a that's reproducible. All of my merge events are inserts. During the merge, there doesn't seem to be any pending inserts, deletes, or updates on the background context. I was initially using performBlockAndWait all over the place until I learned about the difference between performBlock and performBlockAndWait from the WWDC video. I switched to performBlock, and that made it a little bit better. Initially I approached this as a threading issue, diverged into the possibility of it being a weird memory problem caused by not fully understanding blocks, and now I'm back to it being a race condition. It seems like there's just one piece that I'm missing. There are two ways it doesn't happen...

(1) Register for the context will save notification, nil out the FRC delegate when I get it, and set the delegate back after the merge. This isn't far from not using an FRC at all, so this really isn't an option for a workaround.

(2) Do things that block the main thread long enough, so the race condition doesn't happen. For example, when I add a lot of debug log messages to my table view delegate, that slows it down enough for it not to happen.

Here are what I believe to be the important pieces of code (I've shortened certain spots to shrink this already large post).

After various points during scrolling, the view controller will request more data by calling a function that has this in it...

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

For the most part, parseJSON doesn't really have anything interesting in it...

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

The implementation for save and merge are where it might get interesting. Save expects to be called from within the appropriate performBlock already, so it doesn't do anything with the 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);
        }
    }
}

Upon saving, the merge notification gets triggered. I'm only merging from background to main, so I pretty much just want to know if I can inline the merge call or if I need to do it inside of performBlock.

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

My fetched results controller delegate methods are pretty boiler plate stuff...

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

One other piece of code that might be of interest. I'm using autolayout for my table view cells, and the new estimatedHeightForRowAtIndexPath API for dynamic cell height. What this means is that during the call to [self.tableView endUpdates], the last step actually reaches down into some managed objects, whereas the other calls for number of sections/rows only need to know counts from the 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;
}

Been stuck on this for almost a week now. Learned a ton in the process, but geez I'm ready to move on. Any suggestions would be greatly appreciated.

Edit

I put together a pretty big debug log to try to tell the story of what's going on with the udpates. I'm seeing something really strange. I'm updating the table with 50 rows at a time, so I'll only include the interesting part of my debug output. Every time a cell gets configured I'm printing out what the title was for the cell that I just dequeued as well as what the new title will be. When I hit the last cell in the table view, I make a query to the web service for more data. This output is related to the final update before I hit the 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

In the final update it's attempting to insert at s5 r17, but I already had a cell at that row. It also attempts to insert at s5 r22, but I also already had a cell at that row. Lastly it inserts a row at s5 r25, which actually is a new row. It seems to me as though considering r17 and r22 as inserts is leaving a gap in the table. Shouldn't the previous cells at those indexes have events to be moved to r23 and r24?

My fetched results controller is using a sort descriptor that sorts by date and start time. Maybe the existing events that were at r17 and r22 aren't getting move events because there weren't any changes related to their NSManagedObjects. Essentially, they are required to move because of my sort descriptor for events earlier than them and not because their data changed.

Edit 2:

Looks like those inserts do just trigger the existing cells to shift down :(

Edit 3:

Things I tried today...

  1. Made AFNetworking success block waits for the merge to complete before it returns
  2. Made cellForRowAtIndexPath return a stale cell (essentially dequeue it and return it right away) if the fetched results controller is in the middle of beginUpdates/endUpdates. Thinking that extra random cellForRowAtIndexPath that gets called during the update may have been doing weird things.
  3. Removing the background context altogether. This is interesting. If I do all of the UI updates AND JSON parsing on the main context, it still happens.

Edit 4:

Now it's getting interesting.

I tried removing random components in my table view such as the refresh control. Also tried getting rid of my use of estimatedHeightForRowAtIndexPath, which meant just supplying a static row height instead of using autolayout to determine dynamic row height. Both of those turned up nothing. I also tried getting rid of my custom cell entirely, and just using a basic table view cell.

That worked.

I tried a basic table view cell with subtitle.

That worked.

I tried a basic table view cell with subtitle and image.

That worked.

The top of my stack trace being near all of those animation related items is starting to make more sense. It's looking like this is auto-layout related.

解决方案

From an Apple Technical Support Engineer:

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.

In this case, Core Data caught an exception through your controllerDidChangeContent: method, caused by trying to use insertObject:atIndex.

The most likely fix is to ensure that all your NSManagedObject code is encapsulated inside performBlock: or performBlockAndWait: calls.

In iOS 8 and OSX Yosemite, Core Data gains the ability to detect and report violations of its concurrency model. It works by throwing an exception whenever your app accesses a managed object context or managed object from the wrong dispatch queue. You enable the assertions by passing -com.apple.CoreData.ConcurrencyDebug 1 to your app on the command line via Xcodeʼs Scheme Editor.

Ole Begemann has a great writeup of the new feature.

这篇关于NSFetchedResultsController尝试插入nil对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆