Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why is NSFetchedResultsController's fetchedObjects array not always homogeneous

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.

like image 588
pohl Avatar asked Jan 25 '16 16:01

pohl


1 Answers

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.

like image 75
keithyip Avatar answered Oct 05 '22 23:10

keithyip