设置项目
您可以跳过整个部分,下载xcodeproj。
Create a new project with the Master-Detail Application template.
Add a new attribute to the Event entity. (This is important since we want to be able to update an attribute without it causing the NSFetchedResultsController to reorder any of its items. Otherwise it will send the
NSFetchedResultsChangeMove
event rather than theNSFetchedResultsChangeUpdate
event).Call the attribute
hasMovedUp
, and make it aBoolean
. (Note: it may seem silly to create such an attribute, but this is only an example, and I tried to reduce it to the minimum number of steps needed in order to reproduce this bug.)Add a new entity, call it
EventParent
.Create a relationship to Event, call it
child
. Make the inverse relationship as well, call itparent
. (Note: this is a 1:1 relationship.)Click on EventParent. Click on its child relationship. Set its Delete Rule to Cascade. The idea is that we will only be deleting parent objects. When the parent is deleted, it will automatically delete its child.
Leave the Event's parent relationship Delete Rule as Nullify.
Create NSManagedObject Subclasses via Xcode for both entities.
In the
insertNewObject:
method, where the new Event is created, make sure to create a corresponding parent.In the
Event.m
file, automatically assign the last event'shasMovedUp
to beYES
by declaring aprepareForDeletion
event:NSLog(@"Prepare for deletion"); NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Event"]; NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"timeStamp" ascending:NO]; [fetchRequest setSortDescriptors:@[sortDescriptor]]; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil]; NSAssert(results, nil); Event *lastEvent = results.lastObject; NSLog(@"Updating event: %@", lastEvent.timeStamp); lastEvent.hasMovedUp = @YES; [super prepareForDeletion];
In the Storyboard, delete the segue to the DetailViewController. We won't be needing it.
Add some log statements in the
didChangeObject
event in the case of aNSFetchedResultsChangeDelete
andNSFetchedResultsChangeUpdate
. Have it outputindexPath.row
.Finally, make it so that when a cell is tapped, its corresponding parent is deleted. Do this by creating the
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
in theMasterViewController.m
file:NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext]; Event *event = [self.fetchedResultsController objectAtIndexPath:indexPath]; EventParent *parent = event.parent; NSLog(@"Deleting event: %@", event.timeStamp); [context deleteObject:parent]; //[context deleteObject:event]; // comment and uncomment this line to reproduce or fix the error, respectively.
目前的设置概要:
- 我们不会过多操作NSFetchedResultsController。我们将允许它观察和显示事件。
- 每当我们删除一个EventParent时,我们希望相应的Event也被删除。
- 为了增加一些复杂性,我们希望在删除事件时更新
hasMovedUp
属性。
重现错误
Run the App
Create 2 records by tapping the plus button twice.
Tap the top record and watch the app crash (Note: 95% of the time it will crash. If it doesn't crash for you, restart the app until it does). Here are some useful NSLogs:
2013-07-09 13:38:26.984 ReproNFC_PFD_bug[9518:11603] Deleting event: 2013-07-09 20:28:30 +0000 2013-07-09 13:38:26.986 ReproNFC_PFD_bug[9518:11603] Prepare for deletion 2013-07-09 13:38:26.987 ReproNFC_PFD_bug[9518:11603] Updating event: 2013-07-09 02:48:49 +0000 2013-07-09 13:38:26.989 ReproNFC_PFD_bug[9518:11603] Delete detected on row: 0 2013-07-09 13:38:26.990 ReproNFC_PFD_bug[9518:11603] Update detected on row: 1
Now uncomment the
[context deleteObject:event]
line above.Run the app and notice that it no longer crashes. The logs:
2013-07-09 13:20:19.917 ReproNFC_PFD_bug[8997:11603] Deleting event: 2013-07-09 20:20:03 +0000 2013-07-09 13:20:19.919 ReproNFC_PFD_bug[8997:11603] Prepare for deletion 2013-07-09 13:20:19.921 ReproNFC_PFD_bug[8997:11603] Delete detected on row: 0 2013-07-09 13:20:19.924 ReproNFC_PFD_bug[8997:11603] Updating event: 2013-07-09 02:48:49 +0000 2013-07-09 13:20:19.925 ReproNFC_PFD_bug[8997:11603] Update detected on row: 0
日志中有两个不同的地方:
在更新下一个事件之前,删除被检测到。
更新发生在第0行(正确的行),而不是第1行(错误的行)。继续阅读以了解为什么0是正确的数字。
(注意:即使在我们预计出现错误但实际上没有出现错误的5%时间内,日志事件也会按照完全相同的顺序输出。)
异常
异常会在configureCell:atIndexPath:
方法的以下行引发:
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
它引发异常的原因是在不再存在的行上检测到了更新(1)。请注意,当异常未发生时,更新会在正确的行上被检测到(0),因为顶部行已被删除,底部行现在位于索引0。引发的异常为: CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. *** -[_PFBatchFaultingArray objectAtIndex:]: index (19789522) beyond bounds (2) with userInfo (null). * Terminating app due to uncaught exception 'NSRangeException', reason: '* -[_PFBatchFaultingArray objectAtIndex:]: index (19789522) beyond bounds (2)'.
含义
这似乎表明依赖级联删除规则并不等同于显式地自行删除对象。
换句话说...
这个:
[context deleteObject:parent];
// parent will auto-delete the corresponding Event via a cascade rule
…不同于这个:
[context deleteObject:parent];
[context deleteObject:event];
解决方法
更新 6/9/13:
Xcodeproj 已更新,包含多个不同解决方法的 #define
语句(在 Event.h 文件中)。将这三个都保持未定义状态即可重现错误。定义其中任意一个,即可实现相应的解决方法。目前有三种解决方法:A、B 和 C。
A:显式调用 delete
这种解决方法与上面提到的内容重复,但出于完整性考虑,在此也一并列出。
通过不依赖级联删除,而是自己调用 delete,就可以使所有东西正常工作:
// (CUSTOMIZATION_POINT A)
[context deleteObject:parent]; // A1: this line should always run
#ifdef Workaround_A
[context deleteObject:event]; // A2: this line will fix the bug
#endif
日志:
2013-07-09 13:20:19.917 ReproNFC_PFD_bug[8997:11603] Deleting event: 2013-07-09 20:20:03 +0000
2013-07-09 13:20:19.919 ReproNFC_PFD_bug[8997:11603] Prepare for deletion
2013-07-09 13:20:19.921 ReproNFC_PFD_bug[8997:11603] Delete detected on row: 0
2013-07-09 13:20:19.924 ReproNFC_PFD_bug[8997:11603] Updating event: 2013-07-09 02:48:49 +0000
2013-07-09 13:20:19.925 ReproNFC_PFD_bug[8997:11603] Update detected on row: 0
B: 使用@MartinR的建议:
通过忽略indexPath
参数,仅在didChangeObject:
方法中使用anObject
参数,您可以规避该问题:
case NSFetchedResultsChangeUpdate:
NSLog(@"Update detected on row: %d", indexPath.row);
// (CUSTOMIZATION_POINT B)
#ifndef Workaround_B
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; // B1: causes bug
#else
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject]; // B2: doesn't cause bug
#endif
break;
然而,日志仍然显示顺序混乱:
2013-07-09 13:24:43.662 ReproNFC_PFD_bug[9101:11603] Deleting event: 2013-07-09 20:24:42 +0000
2013-07-09 13:24:43.663 ReproNFC_PFD_bug[9101:11603] Prepare for deletion
2013-07-09 13:24:43.666 ReproNFC_PFD_bug[9101:11603] Updating event: 2013-07-09 02:48:49 +0000
2013-07-09 13:24:43.667 ReproNFC_PFD_bug[9101:11603] Delete detected on row: 0
2013-07-09 13:24:43.667 ReproNFC_PFD_bug[9101:11603] Update detected on row: 1
这让我相信这个解决方案可能会在代码的其他部分引起相关问题。
C:在prepareForDelete中使用0秒延迟:
如果在prepare for delete后使用0秒延迟更新对象,这将绕过该错误。
- (void)updateLastEventInContext:(NSManagedObjectContext *)context {
// warning: do not call self.<anything> in this method when it is called with a delay, since the object would have already been deleted
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Event"];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"timeStamp" ascending:NO];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
NSArray *results = [context executeFetchRequest:fetchRequest error:nil];
NSAssert(results, nil);
Event *lastEvent = results.lastObject;
NSLog(@"Updating event: %@", lastEvent.timeStamp);
lastEvent.hasMovedUp = @YES;
}
- (void)prepareForDeletion {
NSLog(@"Prepare for deletion");
// (CUSTOMIZATION_POINT C)
#ifndef Workaround_C
[self updateLastEventInContext:self.managedObjectContext]; // C1: causes the bug
#else
[self performSelector:@selector(updateLastEventInContext:) withObject:self.managedObjectContext afterDelay:0]; // C2: doesn't cause the bug
#endif
[super prepareForDeletion];
}
此外,日志顺序似乎是正确的,因此您可以恢复在NSFetchedResultsController上调用indexPath(即,您不需要使用解决方法B):
2013-07-09 13:27:38.308 ReproNFC_PFD_bug[9196:11603] Deleting event: 2013-07-09 20:27:37 +0000
2013-07-09 13:27:38.309 ReproNFC_PFD_bug[9196:11603] Prepare for deletion
2013-07-09 13:27:38.310 ReproNFC_PFD_bug[9196:11603] Delete detected on row: 0
2013-07-09 13:27:38.319 ReproNFC_PFD_bug[9196:11603] Updating event: 2013-07-09 02:48:49 +0000
2013-07-09 13:27:38.320 ReproNFC_PFD_bug[9196:11603] Update detected on row: 0
然而,这意味着您不能在
updateLastEventInContext:
方法中访问self.timeStamp
,例如,因为对象在那一点上已经被删除(这是假设您在调用删除父对象后立即保存上下文)。
didChangeObject:...
的anObject
参数。将该解决方法应用于您的示例项目后,它不再崩溃。 - Martin R