NSFetchedResultsController 发现错误数量的对象

NSFetchedResultsController finds wrong number of objects

我看到 NSFetchRequest returns 不同数量的对象,具体取决于它是直接通过 NSManagedObjectContext 执行还是作为构建 NSFetchedResultsController 的一部分执行。

示例代码:

- (void)setupResultsController {
    NSError *error = nil;
    NSManagedObjectContext *ctx = [[DataManager sharedInstance] mainObjectContext];

    // Create a fetch request and execute it directly
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

    NSEntityDescription *entity = [Song entityInManagedObjectContext:ctx];
    [fetchRequest setEntity:entity];
    [fetchRequest setFetchBatchSize:20];

    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"section" ascending:YES];
    NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSArray *sortDescriptors = @[sortDescriptor, nameDescriptor];

    [fetchRequest setSortDescriptors:sortDescriptors];
    NSArray *debugResults = [ctx executeFetchRequest:fetchRequest error:&error];
    NSLog(@"Count from context fetch: %lu", (unsigned long)debugResults.count);

    // Use the request to populate a NSFetchedResultsController
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
                                                             initWithFetchRequest:fetchRequest
                                                             managedObjectContext:ctx
                                                             sectionNameKeyPath:@"section"
                                                             cacheName:@"Detail"];
    [aFetchedResultsController performFetch:&error];
    NSLog(@"Count from results controller fetch: %lu", (unsigned long)[[aFetchedResultsController fetchedObjects] count]);

    _songResultsController = aFetchedResultsController;
}

在日志消息中执行上述结果:

2020-01-10 11:05:07.892772-0500 asb7[12985:105052] 从上下文获取计数:10

2020-01-10 11:05:07.893259-0500 asb7[12985:105052] 从结果控制器获取计数:9

两次提取的区别在于 NSFetchedResultsController 缺少最近添加的对象。一个极端奇怪的地方是,在 运行 将应用程序运行一些看似随机的次数后,计数开始一致并获取新对象。

编辑:

如果我将 nil 作为缓存名称传递,或者如果我删除第二个排序描述符,结果会变得一致。显然,这些会导致不良的行为变化,但可能是线索。

NSFetchedResultsController 似乎将过时的缓存视为有效。更改排序描述符会使缓存无效,但是更新持久存储文件 应该 会使它无效,但在这种情况下显然不会。

经过更多的试验,我有一个解释...如果不是解决方案的话。添加新对象不会更改我的 .sqlite 文件的修改日期。它更新了 .sqlite-shm 和 .sqlite-wal 但我猜在判断是否使用缓存时不会考虑这些。在终端会话中使用 touch 可使问题在下次启动时消失。

(Xcode 10.1,macOS 10.13.6,部署目标 10.3,iOS 12.1 模拟器和 10.3.2 设备)

另一次编辑:

我已经在 https://github.com/PhilKMills/CacheTest

上传了一个演示问题的压缩项目目录

我得到的是:第一个 运行,两次提取都有 3 条记录;第二个 运行、6 和 3。我认为这完全有可能取决于我的特定软件版本,但我目前无法升级。其他人的结果将是最有趣的。

注意:如果没有分配 FRC 委托,则不会出现此问题。

我怀疑问题与 FRC 缓存的使用有关,尽管我不确定为什么会这样。因此,一种解决方法是放弃使用 FRC 缓存。然而,这并不理想。

经过 OP 的进一步实验,问题似乎与与持久存储关联的 .sqlite 文件上的时间戳有关。通过推断(*),如果 FRC 检测到时间戳已更改,它会意识到其缓存可能已过时,并重建它 - 从而检测到新添加的对象。但是,因为 CoreData 默认使用 SQLite 的 "WAL journal mode"(参见 here and here),数据库更新不会写入 .sqlite 文件,而是写入一个单独的 .sqlite-wal 文件:时间戳因此,.sqlite 文件上的内容不会更改,即使数据库已更新。 FRC 继续使用它的缓存,不知道额外的对象(或者实际上其他的变化)。

因此,第二种解决方法是放弃使用 SQLite 的 WAL 日志模式。这可以通过在加载持久存储时指定所需的日志模式来实现,使用

NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}

参见 here

(*) 事实上,这是有记录的 here:

“the controller tests the cache to determine whether its contents are still valid. The controller compares the current entity name, entity version hash, sort descriptors, and section key-path with those stored in the cache, as well as the modification date of the cached information file and the persistent store file.”