当删除的原因是级联删除规则时,每当prepareForDelete更新模型时, NSFetchedResultsController似乎存在错误。
这似乎暗示隐式删除(通过级联删除)的行为与显式删除非常不同。
这真的是一个错误,还是你能解释为什么我看到这些奇怪的结果?
设置项目
您可以跳过这整个部分并下载 xcodeproj。
使用Master-Detail Application模板创建一个新项目。
向Event实体添加一个新属性。(这很重要,因为我们希望能够更新一个属性,而不会导致 NSFetchedResultsController 重新排序它的任何项目。否则它将发送
NSFetchedResultsChangeMove
事件而不是NSFetchedResultsChangeUpdate
事件)。调用属性
hasMovedUp
,并将其设为Boolean
. (注意:创建这样一个属性可能看起来很愚蠢,但这只是一个示例,我试图将其减少到重现此错误所需的最少步骤数。)添加一个新实体,调用它
EventParent
。创建与Event的关系,调用它
child
。也建立反比关系,称之为parent
。(注意:这是 1:1 的关系。)单击事件父级。单击其子关系。将其Delete Rule设置为Cascade。这个想法是我们只会删除父对象。当父级被删除时,它会自动删除它的子级。
将 Event 的父关系 Delete Rule 保留为Nullify。
通过 Xcode 为这两个实体创建 NSManagedObject 子类。
在
insertNewObject:
创建新事件的方法中,确保创建相应的父级。在文件中,通过声明一个事件
Event.m
自动分配最后一个事件:hasMovedUp
YES
prepareForDeletion
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];
在 Storyboard 中,删除 DetailViewController 的 segue。我们将不需要它。
在 a和
didChangeObject
的情况下在事件中添加一些日志语句。让它输出。NSFetchedResultsChangeDelete
NSFetchedResultsChangeUpdate
indexPath.row
最后,使当一个单元格被点击时,其对应的父级被删除。通过
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
在MasterViewController.m
文件中创建: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
删除事件时更新属性。
重现错误
运行应用程序
通过点击加号按钮两次创建 2 条记录。
点击顶部记录并观察应用程序崩溃(注意:95% 的时间它会崩溃。如果它没有为您崩溃,请重新启动应用程序直到它崩溃)。以下是一些有用的 NSLog:
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
现在取消注释
[context deleteObject:event]
上面的行。运行应用程序并注意它不再崩溃。日志:
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:错误:严重的应用程序错误。在核心数据更改处理期间捕获到异常。这通常是 NSManagedObjectContextObjectsDidChangeNotification 观察者中的一个错误。*** -[_PFBatchFaultingArray objectAtIndex:]: index (19789522) 越界 (2) with userInfo (null)
.
* 由于未捕获的异常“NSRangeException”而终止应用程序,原因:“* -[_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];
解决方法
2013 年 6 月 9 日更新:
Xcodeproj已更新以包含针对可用的不同解决方法的多个语句#define
(在Event.h文件中)。保留所有 3 个未定义以重现该错误。定义其中任何一个以查看实施的特定解决方法。到目前为止,共有三种解决方法:A、B 和 C。
A:显式调用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
参数,只anObject
在方法中使用参数didChangeObject:
,可以规避问题:
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 秒延迟:
如果您在准备删除的零秒延迟后更新对象,这将绕过该错误:
- (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
但是,这意味着您无法访问self.timeStamp
,例如,在updateLastEventInContext:
方法中,因为该对象此时已经被删除(这是假设您在调用删除父对象后立即保存上下文)。