核心数据可转换属性 (NSArray) 为空

Core Data Transformable Attributes (NSArray) is empty

将 NSArray 保存到可转换的核心数据属性时,该对象将无法在后续获取其实体时访问。但是,之后的任何提取都可以使用它。怎么回事?

我可以在 iOS 应用程序的一个位置设置和保存核心数据实体及其属性。然后我去阅读最近保存的实体。除了可转换的 NSArrays 之外的所有属性都可用。由于某种原因,数组显示为空(在日志中打印时它看起来像这样:route = "(\n)"。如果应用程序关闭然后再次打开,该属性不再为空。有什么想法吗?

我知道将 NSArray 保存到可转换属性是不是最佳实践。你能解释一下为什么会这样吗?


更新 1

NSArray 中填充了 CLLocation 对象。

控制台中没有打印任何错误或警告。他们也没有任何编译器警告或错误。


更新 2

下面是我为这个问题写的一个XCTest。直到最后一个断言(如预期的那样),测试才会失败。

- (void)testRouteNotNil {
    // This is an example of a performance test case.
    NSMutableArray *route;
    for (int i = 0; i < 500; i++) {
        CLLocation *location = [[CLLocation alloc] initWithLatitude:18 longitude:18];
        [route addObject:location];
    }
    NSArray *immutableRoute = route;

    // Save the workout entity
    //   Just use placeholder values for the XCTest
    //   The method below works fine, as the saved object exists when it is fetched and no error is returned.
    NSError *error = [self saveNewRunWithDate:@"DATE01" time:@"TIME" totalSeconds:100 distance:[NSNumber numberWithInt:100] distanceString:@"DISTANCE" calories:@"CALORIES" averageSpeed:[NSNumber numberWithInt:100] speedUnit:@"MPH" image:[UIImage imageNamed:@"Image"] splits:route andRoute:immutableRoute];
    XCTAssertNil(error);

    // Fetch the most recently saved workout entity
    RunDataModel *workout = [[[SSCoreDataManager sharedManager] fetchEntityWithName:@"Run" withSortAttribute:@"dateObject" ascending:NO] objectAtIndex:0];
    XCTAssertNotNil(workout);

    // Verify that the fetched workout is the one we just saved above
    XCTAssertEqual(workout.date, @"DATE01");

    // Check that the any non-NSArray object stored in the entity is not nil
    XCTAssertNotNil(workout.distance);

    // Check that the route object is not nil
    XCTAssertNotNil(workout.route);
}

更新 3

正如您在下面看到的,这就是 Xcode 中 Core Data 模型的设置方式。选择路由属性。请注意,我已经尝试过使用和不使用瞬态 属性。我需要添加一个 Value Transformer Name 吗?


更新 4

Core Data 管理代码本身来自我的 GitHub 存储库,SSCoreDataManger(据我所知它运行良好)。

这里是saveNewRunWithDate方法:

- (NSError *)saveNewRunWithDate:(NSString *)date time:(NSString *)time totalSeconds:(NSInteger)totalSeconds distance:(NSNumber *)distance distanceString:(NSString *)distanceLabel calories:(NSString *)calories averageSpeed:(NSNumber *)speed speedUnit:(NSString *)speedUnit image:(UIImage *)image splits:(NSArray *)splits andRoute:(NSArray *)route {
    RunDataModel *newRun = [[SSCoreDataManager sharedManager] insertObjectForEntityWithName:@"Run"];
    newRun.date = date;
    newRun.dateObject = [NSDate date];
    newRun.time = time;
    newRun.totalSeconds = totalSeconds;
    newRun.distanceLabel = distanceLabel;
    newRun.distance = distance;
    newRun.calories = calories;
    newRun.averageSpeed = speed;
    newRun.speedUnit = speedUnit;
    newRun.image = image;
    newRun.splits = splits; // This is also an issue
    newRun.route = route; // This is an issue
    return [[SSCoreDataManager sharedManager] saveObjectContext];
}

下面是 RunDataModel NSManagedObject 接口:

/// CoreData model for run storage with CoreData
@interface RunDataModel : NSManagedObject

@property (nonatomic, assign) NSInteger totalSeconds;
//  ... 
// Omitted most attribute properties because they are irrelevant to the question
//  ...
@property (nonatomic, strong) UIImage *image;

/// An array of CLLocation data points in order from start to end
@property (nonatomic, strong) NSArray *route;

/// An array of split markers from the run
@property (nonatomic, strong) NSArray *splits;

@end

在实现中,这些属性是使用 @dynamic

设置的

"transformable" 实体属性是通过 NSValueTransformer 实例的实体属性。用于特定属性的 NSValueTransformer class 的名称在托管对象模型中设置。当 Core Data 访问属性数据时,它将调用 +[NSValueTransformer valueTransformerForName:] 来获取值转换器的实例。使用该值转换器,存储在实体存储中的 NSData 将转换为通过托管对象实例的 属性 访问的对象值。

您可以在核心数据编程指南部分阅读更多相关信息 Non-Standard Persistent Attributes

默认情况下,Core Data 使用为名称 NSKeyedUnarchiveFromDataTransformerName 注册的值转换器,并使用它 反向 来执行转换。如果在核心数据模型编辑器中没有指定值转换器名称,就会发生这种情况,并且通常是您想要的行为。如果您想使用不同的 NSValueTransformer,您必须通过调用 +[NSValueTransformer setValueTransformer:forName:] 在您的应用程序中注册它的名称,并在模型编辑器(或代码,这是另一回事)中设置字符串名称。请记住,您使用的值转换器必须同时支持正向和反向转换。

默认值转换器可以将任何支持键控归档的对象转换为NSData。在您的情况下,您有一个 NSArray(实际上是一个 NSMutableArray,这并不好)。 NSArray 支持 NSCoding,但由于它是一个集合,因此其中包含的对象也必须支持它 - 否则无法存档。幸运的是,CLLocation 确实支持 NSSecureCodingNSCoding.

的更新变体

您可以使用 Core Data 的转换器轻松测试 NSArrayCLLocation 的转换。例如:

- (void)testCanTransformLocationsArray {
    NSValueTransformer  *transformer        = nil;
    NSData              *transformedData    = nil;

    transformer = [NSValueTransformer valueTransformerForName:NSKeyedUnarchiveFromDataTransformerName];
    transformedData = [transformer reverseTransformedValue:[self locations]];
    XCTAssertNotNil(transformedData, @"Transformer was not able to produce binary data");
}

我鼓励您为可转换属性编写此类测试。很容易对您的应用程序进行与默认转换器不兼容的更改(例如插入不支持键控归档的对象)。

使用这样的一组测试,我无法重现归档 CLLocationNSArray 的任何问题。

你的问题有一个非常重要的部分:

For some reason the arrays show up as empty (when printed in the log it looks like this: route = "(\n)". If the app closes and then opens again, the attribute is no longer empty. Any ideas?

这表明(至少在您的应用程序中,也许不是您的测试中)数据 正在 被转换并应用于商店中的实体。当应用程序设置 routes 值时,数组将持久保存到存储中 - 我们知道这一点,因为下次启动应用程序时数据会出现。

这通常表示应用程序在上下文之间通信更改时出现问题。从您发布的代码来看,似乎 您正在使用单个上下文,并且仅来自主线程 - 否则您的 SSCoreDataManager 单例将无法正常工作,并且它正在使用过时的线程限制并发模型。

同时有地方SSCoreDataManager正在使用-performBlock:访问单个NSManagedObjectContextperformBlock: 应该 与使用队列并发类型创建的上下文一起使用。此处使用的上下文是使用 -init 创建的,它只是包装 -initWithConcurrencyType: 并传递值 NSConfinementConcurrencyType。因此,您肯定会在单例中遇到并发问题,这很可能会导致您看到的某些行为。您在实体上保留一个属性值,但后来当 属性 包装该属性在托管对象上下文中触发错误时看不到该值。

如果您能够使用 Xcode 6.x 和 iOS 8 进行开发,通过传递启动参数打开核心数据并发调试

-com.apple.CoreData.ConcurrencyDebug 1

到您的应用程序。这应该会使这里的一些问题对您更加明显,尽管仅在使用 -init 创建的上下文上调用 performBlock: 应该已经导致抛出异常。如果您的应用程序正在做一些吞噬可能隐藏这个和更多问题的异常。

从您的问题中不清楚您是仅在尝试在调试器中访问 routes 时看到此信息,还是在使用它时也看到功能损坏。在调试托管对象时,您必须非常注意何时在 属性 值上触发错误。在这种情况下,您在调试器中看到一个空数组可能只是因为以不会导致故障触发的方式访问它 - 这将是正确的行为。从您对其他应用程序行为的描述来看,这似乎确实是您的问题的极限 - 毕竟,值被正确保存。

不幸的是,Core Data 编程指南 barely mentions what a fault is, and does so side by side with uniquing. Faulting is a fundamental part of Core Data - it's most of the point of using it - and has almost nothing to do with uniquing. Fortunately, several years ago the Incremental Store Programming Guide 更新了很多关于 Core Data 内部结构的见解,包括故障。

你的测试和单例还有其他问题,不幸的是超出了这个问题的范围。

问题可能在于未删除测试 运行 之间的旧存储。您正在检查的对象可能与您刚刚添加的对象不同。还要确保瞬态 属性 是 而不是 集。临时属性不会持久化。

以下是测试中可能发生的情况。

  1. 在某些时候,您创建了新的 运行 没有路线并保存。
  2. 在下一次测试中 运行 您正在创建另一个具有相同日期 运行 的对象 DATE01
  3. 您不是检查刚创建的对象的 route 属性,而是按日期排序提取。
  4. 您的所有路线都有相同的日期,因此按日期排序基本上不会影响排序结果。
  5. 您的提取结果的第一个对象恰好是一些您未设置路由的旧对象 属性。

以防万一,在 -saveNewRunWithDate:... 方法中记录 newRun.route 值。

NSMutableArray *route = [NSMutableArray array];

您不应该在向可变数组添加对象之前对其进行初始化吗? 您应该添加一个测试来查看数组是否为 nil。

@quellish 的 提供了有关 Core Data 错误的信息以及其中的一些细微差别和技巧。在做了一些挖掘之后,在那个答案的帮助下,我找到了一个解决方案。

在获取所需的(问题)实体之前,刷新 NSManagedObjectContext 中的 NSManagedObject:

[self.managedObjectContext refreshObject:object mergeChanges:NO];

这会更新托管对象的持久属性以使用持久存储中的最新值。它还将对象变成故障。

我遇到了类似的问题,我发现很难解决。最后我确实解决了它,但不是这里的解决方案解决了它。为了与我面临同样挑战的任何人,我想分享我发现的有效方法。

我的解决方案来自这里:Core Data not saving transformable NSMutableDictionary

在我的例子中,问题是因为我试图使用 NSMutableArray 作为可转换的核心数据属性。但我现在明白你不应该那样做。相反,您应该使用不可变数组(即 NSArray),然后,如果您需要更改数组中的值,则将 Core Data 数组复制到本地可变数组(即 Swift 中的 var NSArray),使更改为本地数组,然后 运行 命令使核心数据数组等于更改后的本地数组。然后正常保存核心数据。

正如我所说,我的问题与这里的问题类似,但又不一样。所以我并不是说这是解决这个问题的方法。我只是为了其他人的利益而分享这个,以防对他们有帮助。