簡體   English   中英

核心數據可轉換屬性(NSArray)為空

[英]Core Data Transformable Attributes (NSArray) is empty

將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的變換器測試CLLocationNSArray的轉換。 例如:

- (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的任何問題。

你的問題有一個非常重要的部分:

由於某種原因,數組顯示為空(當在日志中打印時,它看起來像這樣: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:應該會導致異常被拋出。 如果您的應用程序正在做一些事情來吞下可能隱藏此問題和更多問題的異常。

只有當您嘗試訪問調試器中的routes時,或者如果您在使用它時也看到功能損壞,您的問題就不清楚了。 調試托管對象時,您必須非常了解何時觸發屬性值的故障。 在這種情況下,您可能只是在調試器中看到一個空數組,因為它是以不會導致錯誤觸發的方式訪問的 - 這將是正確的行為。 根據您對其他應用程序行為的描述,這似乎可能是您的問題的限制 - 畢竟,值正在被持久化。

遺憾的是,“核心數據編程指南” 幾乎沒有提到故障是什么 ,並且與單一的並排。 錯誤是核心數據的基本組成部分 - 它是使用它的最重要的一點 - 並且幾乎與單一數據無關。 幸運的是,幾年前,“ 增量存儲編程指南”更新了對核心數據內部的許多見解,包括故障。

你的測試和單身人士有其他問題,不幸的是超出了這個問題的范圍。

NSMutableArray *route = [NSMutableArray array];

在向對象添加對象之前,是否應該初始化可變數組? 您應該添加一個測試以查看該數組是否為零。

問題可能在於不在測試運行之間刪除舊存儲。 您正在檢查的對象可能與您剛剛添加的對象不同。 還要確保設置transient屬性。 瞬態屬性不會持久化。

這是測試中可能發生的事情。

  1. 在某些時候,您創建了沒有路線的新運行並保存。
  2. 在下一次測試運行期間,您將創建另一個具有相同日期DATE01運行對象。
  3. 您不是檢查剛剛創建的對象的route屬性,而是按日期排序。
  4. 您的所有路線都具有相同的日期,因此按日期排序基本上不會影響排序結果。
  5. 獲取結果的第一個對象恰好是您沒有設置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數組等於更改的本地數組。 然后正常保存核心數據。

正如我所說,我的問題類似於這里的問題,但它不一樣。 所以我並沒有聲稱這是解決這個問題的方法。 我只是為了別人的利益而分享這個,以防萬一。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM