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

问题描述 投票:3回答:5

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

我可以在我的iOS应用程序中的一个位置设置和保存Core Data实体及其属性。然后我去阅读最近保存的实体。除可转换的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模型的方式。选择了route属性。请注意,无论有没有transient属性,我都尝试过它。我需要添加Value Transformer Name,那是什么?


更新4

核心数据管理代码本身来自我的GitHub repo,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设置这些属性

ios objective-c core-data
5个回答
16
投票

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

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

默认情况下,Core Data使用为名称NSKeyedUnarchiveFromDataTransformerName注册的值转换器,并反向使用它来执行转换。如果在Core Data Model Editor中未指定任何值转换器名称,则会发生这种情况,并且通常是您想要的行为。如果你想使用不同的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");
}

我鼓励你为可转换属性编写这样的测试。您可以轻松地更改与默认转换器不兼容的应用程序(例如插入不支持键控归档的对象)。

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

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

由于某种原因,数组显示为空(当在日志中打印时,它看起来像这样:route =“(\ n)”。如果应用关闭然后再次打开,该属性不再为空。任何想法?

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

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

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

如果您能够使用Xcode 6.x和iOS 8进行开发,请通过传递launch参数打开Core Data并发调试

-com.apple.CoreData.ConcurrencyDebug 1

为您的应用程序。这应该使你在这里看到的一些问题更加明显,尽管在用performBlock:创建的上下文中调用-init应该会导致异常被抛出。如果您的应用程序正在做一些事情来吞下可能隐藏此问题和更多问题的异常。

只有当您尝试在调试器中访问routes时,或者如果您在使用它时也看到功能损坏,您的问题就不清楚了。调试托管对象时,您必须非常了解何时触发属性值的故障。在这种情况下,您可能只是在调试器中看到一个空数组,因为它的访问方式不会导致错误 - 这将是正确的行为。根据您对其他应用程序行为的描述,这似乎可能是您的问题的限制 - 毕竟,值正在被持久化。

不幸的是核心数据编程指南barely mentions what a fault is,并且与uniquing并排。错误是核心数据的基本组成部分 - 它是使用它的最重要的一点 - 并且几乎与单一数据无关。幸运的是,几年前Incremental Store Programming Guide更新了核心数据内部的许多见解,包括断层。

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


3
投票
NSMutableArray *route = [NSMutableArray array];

在向对象添加对象之前,是否应该初始化可变数组?您应该添加一个测试以查看该数组是否为零。


1
投票

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

这是测试中可能发生的事情。

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

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


1
投票

@quellish的answer提供有关核心数据故障以及其中的一些细微差别和细节的信息。在做了一些挖掘之后,在这个答案的帮助下,我找到了一个解决方案。

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

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

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


1
投票

我遇到了类似的问题,我觉得很难解决。最后我确实解决了它,但这不是修复它的解决方案。我希望与那些面临同样挑战的人分享我发现的工作。

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

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

正如我所说,我的问题类似于这里的问题,但它不一样。所以我并没有声称这是解决这个问题的方法。我只是为了别人的利益而分享这个,以防万一。

© www.soinside.com 2019 - 2024. All rights reserved.