使用 NSUserDefaults 儲存字典的一個坑

知識小集發表於2018-04-24

前些時間,@Vong 同學在我們知識小叢集裡發了一段用 NSUserDefaults 儲存一個 NSDictionary 字典物件的測試程式碼,雖然程式碼看起來似乎很正常,但是執行的時候報錯了,根據群裡大家的討論結果,本文整理總結一下。

問題

我們先來看一下這段測試程式碼:

- (void)test {
    NSDictionary *dict = @{@1: @"甲",
                           @2: @"乙",
                           @3: @"丙",
                           @4: @"丁"};
    
    [[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"testKey"];
    [[NSUserDefaults standardUserDefaults] synchronize];
}
複製程式碼

上述程式碼先構建了一個 dict 字典物件,然後把它往 NSUserDefaults 裡儲存,看起來似乎沒什麼毛病,但在 Xcode 中執行的時候,卻報如下錯誤:

[User Defaults] Attempt to set a non-property-list object {
    3 = "\U4e19";
    2 = "\U4e59";
    1 = "\U7532";
    4 = "\U4e01";
} as an NSUserDefaults/CFPreferences value for key `testKey`
複製程式碼

難道是因為上述 NSDictionary 物件的 KeyNSNumber 的問題?NSDictionaryKey 一定要為 NSString 嗎?顯然不是的,我們在上述程式碼的 dict 初始化後打個斷點,然後在命令列裡 po dict,是可以正確看到控制檯輸出 dict 物件的內容。再來看一下 NSDictionary 的定義:

@interface NSDictionary<KeyType, ObjectType> (NSDictionaryCreation)

- (instancetype)initWithObjects:(NSArray<ObjectType> *)objects forKeys:(NSArray<id<NSCopying>> *)keys;

...

@end
複製程式碼

我們可以知道,NSDictionaryKey 只要是遵循 NSCopying 協議的就行,顯然 NSNumber 是遵循的(可自行檢視 NSNumber 的定義),因此用 NSNumber 作為 NSDictionaryKey 是沒問題的。

但是,當我們把上述 dictKey 改為字串的形式(注意跟上面的區別),如下:

NSDictionary *dict = @{@"甲": @1,
                       @"乙": @2,
                       @"丙": @3,
                       @"丁": @4};
複製程式碼

此時,dict 是可以正常儲存到 NSUserDefaults 中的。

所以,問題確實是出在 dictKey 上,但到底是什麼原因呢?

原因分析

我們需要從 NSUserDefaultssetObject:forKey: 方法入手分析:

- (void)setObject:(id)value forKey:(NSString *)defaultName;
複製程式碼

通過檢視蘋果文件:https://developer.apple.com/documentation/foundation/nsuserdefaults/1414067-setobject?language=objc ,有這麼一段描述:

The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.

也就是說,能往 NSUserDefaults 裡儲存的物件只能是 property list objects,包括 NSDataNSStringNSNumberNSDateNSArray, NSDictionary,且對於 NSArrayNSDictionary 這兩個容器物件,它們所包含的內容也必需是 property list objects

那麼,什麼是 Property List 呢?我們可以檢視蘋果的這份文件:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/AboutPropertyLists/AboutPropertyLists.html

使用 NSUserDefaults 儲存字典的一個坑

Property lists are based on an abstraction for expressing simple hierarchies of data, ... By convention, each Cocoa and Core Foundation object listed in Table 2-1 is called a property-list object, ...

... If an array or dictionary contains objects that are not property-list objects, then you cannot save and restore the hierarchy of data using the various property-list methods and functions.

And although NSDictionary and CFDictionary objects allow their keys to be objects of any type, if the keys are not string objects, the collections are not property-list objects.

重點看最後一句話,雖然 NSDictionaryCFDictionary 物件的 Key 可以為任何型別(只要遵循 NSCopying 協議即可),但是如果當 Key 不為字串 string 物件時,此時這個字典物件就不能算是 property list objects 了,所以它就往 NSUserDefaults 中儲存,不然就會報錯:

Attempt to set a non-property-list objec ... as an NSUserDefaults/CFPreferences value for key ...
複製程式碼

以上,真相大白。

By the way,不僅僅是 NSUserDefaultssetObject:forKey: 方法,但凡使用系統其他任何方法涉有及到 Property List 物件,都要注意上面的問題。

掃描關注 知識小集

知識小集是一個團隊公眾號,每週都會有原創文章分享,我們的文章都會在公眾號首發。歡迎關注檢視更多內容。

使用 NSUserDefaults 儲存字典的一個坑

另外,我們開了微信群,方便大家溝通交流技術。目前知識小集1號群已滿,新開了知識小集2號群,有興趣的可以掃碼加入,沒有iOS限制,移動開發的同學都歡迎。由於微信群掃碼加群有100的限制,所以如果掃碼無法加入的話,可以先加微訊號 coldlight_hh 或者 wsy9871,再拉進群哈。

使用 NSUserDefaults 儲存字典的一個坑

相關文章