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

时间:2015-01-20 22:56:29

标签: ios objective-c core-data

将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,那是什么?

enter image description here


更新4

核心数据管理代码本身来自我的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

设置这些属性

5 个答案:

答案 0 :(得分:16)

A&#34;可转换&#34; entity属性是通过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的变换器测试NSArray CLLocation的转换。例如:

- (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");
}

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

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

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

  

由于某种原因,数组显示为空(当在日志中打印时,它看起来像这样:route =&#34;(\ n)&#34;。如果应用程序关闭然后再次打开,则属性为不再是空的。有什么想法吗?

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

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

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

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

NSConfinementConcurrencyType

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

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

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

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

答案 1 :(得分:3)

NSMutableArray *route = [NSMutableArray array];

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

答案 2 :(得分:1)

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

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

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

答案 3 :(得分:1)

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

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

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

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

答案 4 :(得分:1)

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

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

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

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