7

So that I don't bury the lede, I'm going to open with my core question: why is it that my NSFetchedResultsController's fetchedObjects array is usually homogeneous, but on rare occasions contains an __NSCFString among the managed objects it should contain?

I have an app that has been in production for a long, long time. It's primary view is a table view that contains a list of videos, backed by core data managed objects. The table view controller uses an NSFetchedResultsController configured with a fairly ordinary NSFetchRequest:

NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:[ABCVideo entityName]];
NSString *sectionKeyPath = nil;
request.fetchBatchSize = 20;
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:ABCVideoAttributes.recordingDate ascending:NO];
            sectionKeyPath = @"sectionIdentifier";
request.sortDescriptors = @[sort];
request.predicate = [NSPredicate predicateWithFormat:@"owner = %@ and %K = %@", person, ABCVideoAttributes.serverDeleted, @(NO)];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:sectionKeyPath cacheName:kABCMyVideosTableViewControllerCacheKey];

Since these videos can be uploaded into the cloud, this table view controller gets occasional notifications to update progress bars in the table view cells corresponding to videos that are currently being uploaded. Inside this callback, we are getting the NSFetchedResultsController's fetchedObjects array to find the video corresponding to the notification, so that the correct table view cell can update its progress bar.

This all works. 99.9% of the time, it works every time </RonBurgundy>.

But I noticed in our HockeyApp crash reports that there is a rare, rare case where I was getting a SIGABRT that happens when my notification handler is trying to get a filteredArrayUsingPredicate from the fetchedObjects:

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0x136f24480> valueForUndefinedKey:]: this class is not key value coding-compliant for the key guid.'

Recently I managed to find a case where I could occasionally reproduce this crash, and after a lot of experimentation, I discovered that the fetchedObjects array sometimes contained something that wasn't an ABCVideo: instead, one slot in the array was occupied by a __NSCFString instance. This was pretty suprising, given that the NSFetchRequestResultType is of NSManagedObjectResultType, and a string is not a managed object.

So I'm left wondering: is this a Core Data bug? Or does my array contain a pointer that formerly pointed to an ABCVideo instance that was deallocated and that location on the heap was subsequently taken by a __NSCFString instance? If it's the latter, then how could this happen? I'm using ARC, so it's hard to understand how one of these videos could become deallocated.

pohl
  • 3,158
  • 1
  • 30
  • 48
  • I suspect a Core Data bug. iOS9 introduced several bugs, especially with NSFetchedResultsControllers, and this may be another. – Avi Jan 25 '16 at 16:32
  • The array itself should retain it's objects so unless you're somehow `release`ing one-too-many, they should not be deallocated. Also, with ARC, this is almost certainly not the case. Could be an ARC bug or a Core Data bug. Can you rule out that this is related to caching with the `NSFetchedResultsController`? Disable caching and reproduce? – Tobi Nary Jan 25 '16 at 16:34
  • Since you can reproduce it, it might be useful to detect when this happens and examine the contents of the string. I don't know why the string is there, but its contents might point toward a cause. – Tom Harrington Jan 25 '16 at 17:09
  • 1
    Most likely there is a pointer that used to point to an NSManagedObject (subclass), and now points to a location in memory that is occupied by an NSString object. The real question is: where do you access this pointer, and why is it no longer pointing to an NSManagedObject? Are you working with this array across threads? Ara you keeping your own references somewhere? Can you share some more about exactly what happens? This does not seems like a Core Data bug to me. – Joride Jan 25 '16 at 17:41
  • @JanGreve It takes a while to set up each attempt at reproduction, and it doesn't trip the bug every time, but I was able to set up 9 attempts (5 with caching disabled and 4 with caching enabled). Even with no cache, I get a crash, although it changes to EXC_BAD_ACCESS instead of the above SIGABRT. (Sadly, if I enable zombie objects, I can't reproduce the problem at all.) – pohl Jan 25 '16 at 20:49
  • @Joride I'm leaning towards your narrative, although this array is only read from, and only from the main thread (so far as I can tell). References to the array's contents are never kept. – pohl Jan 25 '16 at 20:51
  • @TomHarrington the strings, when I have seen them, are strings that would have otherwise been properties of the ABCVideo managed object. It's not always from the same property. – pohl Jan 25 '16 at 20:52
  • Can you post all code inside the file/class where the fetcherestsconttroller is used? If too big, maybe just the methods actually using the fetchedresultscontroller. Does the frc have a delegate? When is this set to nil? – Joride Jan 25 '16 at 21:05

1 Answers1

8

There is a memory management bug in -[NSFetchedResultsController fetchedObjects] with NSFastEnumeration. The object 0x136f24480 was ABCVideo but deallocated. That piece of memory was used to store __NSCFString. Sending messages to wrong objects, EXC_BAD_ACCESS and SIGABRT are the common result.

This bug is also in my app but I could not reproduce it even once. If you are willing to share a sample project that can reproduce the problem, we can work together to solve it.

There are multiple workarounds. The key is to avoid NSFastEnumeration on fetchedObjects

// 1
NSArray *fetchedObjects = controller.fetchedObjects
for (int i = 0; i < fetchedObjects.count; ++i) {
    NSManagedObject *object = fetchedObjects[i];
}

// 2
NSArray <id<NSFetchedResultsSectionInfo>> *sections = controller.sections;
for (int s = 0; s < sections.count; ++s) {
    id<NSFetchedResultsSectionInfo> section = sections[s];
    for (int i = 0; i < [section numberOfObjects]; ++i) {
        NSManagedObject *object = [controller objectAtIndexPath:[NSIndexPath indexPathForItem:i inSection:s]];
    }
}

// 3 Fetch from NSManagedContextDirectly

If unfortunately, someone reading this is using Swift, you will get crashes even calling fetchedObjects because Swift converts NSArray to Array using NSFastEnumeration.

keithyip
  • 985
  • 7
  • 21
  • 2
    @ChrisStillwell I'm not sure I understand where you're coming from. As the author of the question, I would consider this to be an answer to my question if it is true. (Why is the array not homogeneous? Because spurious pointers to released objects coincidentally point to things things with other types). – pohl Jan 26 '16 at 15:23
  • 1
    This bug has existed since iOS 8.x. I am not sure if there was anyone who reported it to Apple. I guess that at least the reporter did not have any sample projects for Apple. Apple like to pretend that there is no bugs if no sample projects are provided. – keithyip Jan 27 '16 at 03:15
  • 2
    I still get random EXC_BAD_ACCESS crashing in iOS 14.7 using fetchedObjects or even using object(at:). The weird thing right after the crash in the debugger I can access the object at that row without problem: The issue seems to be finally fixed in iOS beta 5. – Mycroft Canner Aug 25 '21 at 11:18