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

Sam*_*cer 3 core-data objective-c ios

将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);
}
Run Code Online (Sandbox Code Playgroud)

更新3

如下所示,这是在Xcode中设置Core Data模型的方式.选择了route属性.请注意,无论有没有transient属性,我都尝试过它.我需要添加一个Value Transformer Name,那是什么?

在此输入图像描述


更新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];
}
Run Code Online (Sandbox Code Playgroud)

以下是RunDataModelNSManagedObject接口:

/// 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
Run Code Online (Sandbox Code Playgroud)

在实现中,使用这些属性进行设置 @dynamic

que*_*ish 17

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

您可以在"核心数据编程指南"的" 非标准持久性属性 "一节中阅读更多相关信息

默认情况下,Core Data使用为名称注册的值转换器,NSKeyedUnarchiveFromDataTransformerName反向使用它来执行转换.如果在Core Data Model Editor中未指定任何值转换器名称,则会发生这种情况,并且通常是您需要的行为.如果要使用不同的NSValueTransformer,则必须通过调用+[NSValueTransformer setValueTransformer:forName:]并在模型编辑器中设置字符串名称(或在代码中,这是另一个问题)在应用程序中注册它的名称.请记住,您使用的值变换器必须支持正向和反向转换.

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

您可以测试的转化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");
}
Run Code Online (Sandbox Code Playgroud)

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

使用一组这样的测试,我无法重现的任何问题,一个归档NSArrayCLLocation秒.

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

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

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

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

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

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

-com.apple.CoreData.ConcurrencyDebug 1

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

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

遗憾的是,"核心数据编程指南" 几乎没有提到故障是什么,并且与单一的并排.错误是核心数据的基本组成部分 - 它是使用它的最重要的一点 - 并且几乎与单一数据无关.幸运的是,几年前," 增量存储编程指南"更新了对核心数据内部的许多见解,包括故障.

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