核心数据可转换属性(NSArray)为空
将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
设置这些属性
“可转换”实体属性是通过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
的新版本。
您可以轻松地使用Core Data的变换器测试CLLocation
的NSArray
的转换。 例如:
- (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"); }
我鼓励你为可转换属性编写类似这样的测试。 您可以轻松地更改与默认转换器不兼容的应用程序(例如插入不支持键控归档的对象)。
使用这样的一组测试,我无法重现归档CLLocation
的NSArray
的任何问题。
你的问题有一个非常重要的部分:
由于某种原因,数组显示为空(当在日志中打印时,它看起来像这样:route =“(\ n)”。如果应用关闭然后再次打开,该属性不再为空。任何想法?
这表明(至少在您的应用程序中,可能不是您的测试)数据正在转换并应用于商店中的实体。 当应用程序设置routes
值时,数组会持久存储到商店 – 我们知道这一点,因为下次启动应用程序时会显示数据。
通常,这表示在上下文之间传递更改时应用程序中存在问题。 从您发布的代码看来 ,您使用的是单个上下文,并且只使用主线程 – 否则您的SSCoreDataManager
单例将无法正常工作,并且它使用的是过时的线程限制并发模型。
同时SSCoreDataManager
使用-performBlock:
来访问单个NSManagedObjectContext
。 performBlock:
只应与使用队列并发类型创建的上下文一起使用。 这里使用的上下文是使用-init
创建的, NSConfinementConcurrencyType
包装-initWithConcurrencyType:
并传递值NSConfinementConcurrencyType
。 因此,您肯定会在单例中出现并发问题,这很可能会导致您看到的某些行为。 您将在实体上持久保存属性值,但稍后在包装该属性的属性在托管对象上下文中触发错误时,不会看到该值。
如果您能够使用Xcode 6.x和iOS 8进行开发,请通过传递launch参数打开Core Data并发调试
-com.apple.CoreData.ConcurrencyDebug 1
对您的应用程序。 这应该使一些问题在这里更加明显,尽管在使用-init
创建的上下文中调用performBlock:
应该会导致exception被抛出。 如果你的应用程序正在做一些事情来吞下可能隐藏这个和更多问题的exception。
只有在您尝试访问调试器中的routes
时,或者在使用它时您还看到function损坏时,您的问题还不清楚。 调试托管对象时,您必须非常了解何时触发属性值的故障。 在这种情况下,您可能只在调试器中看到一个空数组,因为它的访问方式不会导致错误 – 这将是正确的行为。 根据您对其他应用程序行为的描述,这似乎可能是您的问题的限制 – 毕竟,值正在被持久化。
遗憾的是,“核心数据编程指南” 几乎没有提及故障是什么 ,并且与单一的并排。 错误是核心数据的基本组成部分 – 它是使用它的最重要的一点 – 并且几乎与单一数据无关。 幸运的是,几年前,“ 增量存储编程指南”更新了对核心数据内部的许多见解,包括故障。
你的测试和单身人士有其他问题,不幸的是超出了这个问题的范围。
NSMutableArray *route = [NSMutableArray array];
在向对象添加对象之前,是否应该初始化可变数组? 您应该添加一个测试以查看该数组是否为零。
问题可能在于不在测试运行之间删除旧存储。 您正在检查的对象可能与您刚刚添加的对象不同。 还要确保未设置transient属性。 瞬态属性不会持久化。
这是测试中可能发生的事情。
- 在某些时候,您创建了没有路线的新运行并保存。
- 在下一次测试运行期间,您将创建另一个具有相同日期
DATE01
运行对象。 - 您没有检查刚刚创建的对象的
route
属性,而是按日期排序。 - 您的所有路线都具有相同的日期,因此按日期排序基本上不会影响排序结果。
- 获取结果的第一个对象恰好是您没有设置routes属性的旧对象。
以防万一,在-saveNewRunWithDate:...
方法中记录newRun.route
值。
@quellish的答案提供了有关核心数据故障以及其中的一些细微差别和细节的信息。 在做了一些挖掘之后,在这个答案的帮助下,我找到了解决方案。
在获取所需(问题)实体之前,刷新NSManagedObject
中的NSManagedObjectContext
:
[self.managedObjectContext refreshObject:object mergeChanges:NO];
这会更新托管对象的持久属性,以使用持久性存储中的最新值。 它还将对象变为故障。
我遇到了类似的问题,我觉得很难解决。 最后我确实解决了它,但这不是修复它的解决方案。 我希望与那些面临同样挑战的人分享我发现的工作。
我的解决方案来自于此: Core Data不保存可转换的NSMutableDictionary
在我的例子中,问题是因为我试图使用NSMutableArray作为可转换的Core Data属性。 但我现在明白你不应该这样做。 相反,您应该使用不可变数组(即NSArray)然后,如果您需要更改数组中的值,则将Core Data数组复制到本地可变数组(即Swift中的var NSArray),对本地进行更改然后运行一个命令,使Core Data数组等于更改的本地数组。 然后正常保存核心数据。
正如我所说,我的问题类似于这里的问题,但它不一样。 所以我并不是说这是解决这个问题的方法。 我只是为了别人的利益而分享这个,以防万一。