前言
在iOS開(kāi)發(fā)中,我們時(shí)常需要保存一些數(shù)據(jù),或者希望在用戶下次打開(kāi)App時(shí),依然可以保留一些設(shè)置等,比如主題設(shè)置、語(yǔ)言設(shè)置,那么我們一般會(huì)選擇使用NSUserDefaults作為輕量級(jí)數(shù)據(jù)持久化方案。這里不對(duì)其他的數(shù)據(jù)持久化方案(如Plist、歸檔、Sqlite3、CoreData等)進(jìn)行探討,主要來(lái)介紹一下NSUserDefaults中的一些黑科技。
NSUserDefaults簡(jiǎn)介
NSUserDefaults在Foundation框架中被定義,用來(lái)存儲(chǔ)和讀取一些輕量級(jí)數(shù)據(jù)。其本質(zhì)是操作plist文件。
它比其他數(shù)據(jù)持久化方案的優(yōu)點(diǎn)在于:
- 輕量級(jí)。
- 方便快捷直接使用。
- 支持
NSData,NSString,NSNumber,NSDate,NSArrayandNSDictionary。
缺點(diǎn)在于:
- 不適合大量數(shù)據(jù)。
- 無(wú)法存儲(chǔ)自定義
model。
黑科技
一些NSUserDefaults最基礎(chǔ)的用法,這里就不贅述了。說(shuō)到黑科技,一般指的是鮮為人知而又十分厲害的技術(shù)。這里主要介紹兩個(gè)黑科技。
還原默認(rèn)值
一般NSUserDefaults使用,都會(huì)保存一些自定義的Key和Value,其實(shí)這個(gè)不涉及還原默認(rèn)值的問(wèn)題。其實(shí)有些時(shí)候,是需要修改其中的系統(tǒng)默認(rèn)值的。比如應(yīng)用內(nèi)切換中英文,就需要修改AppleLanguages的value。既然修改了系統(tǒng)默認(rèn)值,怎么還原默認(rèn)值呢?
我們先來(lái)看一下修改前的:
po [NSUserDefaults standardUserDefaults].dictionaryRepresentation
{
...
AppleLanguages = (
en,
"zh-Hans-US"
);
AppleLocale = "en_US";
...
}
可以看到AppleLanguages對(duì)應(yīng)的是一個(gè)數(shù)組,如果我們?cè)贏pp內(nèi)想切換成中文,執(zhí)行
[[NSUserDefaults standardUserDefaults] setObject:@[@"zh-Hans"] forKey:@"AppleLanguages"];
再次看下修改后
po [NSUserDefaults standardUserDefaults].dictionaryRepresentation
{
...
AppleLanguages = (
zh-Hans
);
AppleLocale = "en_US";
...
}
好吧,這個(gè)AppleLanguages字段的值已經(jīng)被記錄了。這時(shí)候,kill客戶端,重新打開(kāi)客戶端,發(fā)現(xiàn),這個(gè)值肯定不會(huì)變。那么如果這個(gè)時(shí)候用戶在系統(tǒng)設(shè)置中切換到了日語(yǔ),那么我想在客戶端中通過(guò)跟隨手機(jī)系統(tǒng)的設(shè)置,還原到日語(yǔ),該怎么辦呢?
或許你會(huì)想到記錄初始值,只要提前記錄,在還原時(shí)再set回去就可以了。嗯~沒(méi)毛病,可是在什么地方什么時(shí)候記錄初始值呢?這個(gè)AppleLanguages字段其實(shí)就是系統(tǒng)的語(yǔ)言默認(rèn)值,你已經(jīng)把它改過(guò)了,如果在你改過(guò)之后用戶又修改了系統(tǒng)語(yǔ)言,那你是拿不到新默認(rèn)值的,所以這條路是走不通的。
那怎么實(shí)現(xiàn)還原默認(rèn)值呢?看下這個(gè)方法:
/*!
-setObject:forKey: immediately stores a value (or removes the value if nil is passed as the value) for the provided key in the search list entry for the receiver's suite name in the current user and any host, then asynchronously stores the value persistently, where it is made available to other processes.
*/
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
這個(gè)方法Document中并沒(méi)有描述過(guò)多,基本都是在強(qiáng)調(diào)value必須是NSData, NSString, NSNumber, NSDate, NSArray and NSDictionary中的一種,且NSArray and NSDictionary中的值也必須是Plist支持的類型。在注釋中有強(qiáng)調(diào),removes the value if nil is passed as the value,傳nil等同于刪除這個(gè)鍵值對(duì)。
/// -removeObjectForKey: is equivalent to -[... setObject:nil forKey:defaultName]
- (void)removeObjectForKey:(NSString *)defaultName;
其實(shí)這里才是關(guān)鍵
[[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"AppleLanguages"];
//or
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"AppleLanguages"];
然后再次
po [NSUserDefaults standardUserDefaults].dictionaryRepresentation
{
...
AppleLanguages = (
en,
"zh-Hans-US"
);
AppleLocale = "en_US";
...
}
你會(huì)發(fā)現(xiàn),WTF,居然這個(gè)字段還存在,且還原成了默認(rèn)值,厲害了word哥。本方法是本人歷經(jīng)各種坑之后發(fā)現(xiàn)的,且官方文檔中沒(méi)有任何提及,只能說(shuō)Apple爸爸任性。
巧用SuiteName
- (nullable instancetype)initWithSuiteName:(nullable NSString *)suitename NS_AVAILABLE(10_9, 7_0) NS_DESIGNATED_INITIALIZER;
這個(gè)方法,想必各位不會(huì)陌生,在跨App間通信或者主App與Extension共享數(shù)據(jù),都會(huì)用到,一般配合使用的是App Groups。
App Groups數(shù)據(jù)共享
在App Groups中使用,一般兩個(gè)App都需要加入同一個(gè)Group。
然后在兩個(gè)App分別使用
- (NSUserDefaults *)userDefaults
{
return [[NSUserDefaults alloc] initWithSuiteName:@"group.urwork.autosignin"]; //name需要是Group的id
}
來(lái)存儲(chǔ)和讀取數(shù)據(jù),即可實(shí)現(xiàn)數(shù)據(jù)共享。
超微型簡(jiǎn)單方便易用數(shù)據(jù)庫(kù)
這里,其實(shí)主要介紹一下initWithSuiteName:的另一個(gè)用途,我稱之為超微型簡(jiǎn)單方便易用數(shù)據(jù)庫(kù)。
需求:后臺(tái)同學(xué)最近太忙(??這不是理由),需要客戶端同學(xué)臨時(shí)開(kāi)發(fā)一個(gè)本地的瀏覽歷史功能。簡(jiǎn)單的說(shuō)就是在客戶端本地保存每個(gè)用戶瀏覽的文章記錄。
吐糟:好吧,時(shí)間緊,任務(wù)重,功能先上線,后續(xù)再優(yōu)化已經(jīng)是一個(gè)習(xí)慣了??。
實(shí)現(xiàn)方案:簡(jiǎn)單,本地存?zhèn)€字典(或者Plist文件),每個(gè)key是一個(gè)用戶id,value是一個(gè)數(shù)組,數(shù)組里的每個(gè)元素是一個(gè)字典,這個(gè)字典里有articleId、title、imgUrl、...。結(jié)構(gòu)大概是這樣:
{
"10086" : [
{
"articleId" : 1,
"imgUrl" : "https://img.xxx.com/abcdefg.png",
"title" : "中國(guó)歷史",
...
},
{
"articleId" : 2,
"imgUrl" : "https://img.xxx.com/abcdefg.png",
"title" : "中國(guó)教育",
...
},
...
],
"10087" : [
{
"articleId" : 2,
"imgUrl" : "https://img.xxx.com/abcdefg.png",
"title" : "中國(guó)教育",
...
},
...
]
}
看起來(lái)OK,但是小伙伴們,性能問(wèn)題還是要考慮的。我想獲取某個(gè)用戶的瀏覽歷史,需要把文件整體讀到內(nèi)存中,然后用dict[@"10086"]的方法,得到這個(gè)用戶的瀏覽歷史Array,用戶少還好,多了,每次讀取的IO操作會(huì)很耗時(shí),對(duì)性能有影響。
可能你會(huì)說(shuō)用數(shù)據(jù)庫(kù)啊什么的,當(dāng)然可以,但是又是需要依賴一堆第三方,還要寫(xiě)好多代碼,創(chuàng)建好多類。我就是想簡(jiǎn)單實(shí)現(xiàn)一下,有沒(méi)有更好的方法呢?當(dāng)然有,超微型簡(jiǎn)單方便易用數(shù)據(jù)庫(kù)。
每個(gè)用戶都可以用一個(gè)NSUserDefaults來(lái)代替,每個(gè)用戶的瀏覽歷史,可以直接用setObject:forKey:的方式來(lái)存儲(chǔ),如:
//這個(gè)跟App Groups沒(méi)有關(guān)系
//已有則讀取,沒(méi)有則創(chuàng)建
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"10086"];
//存儲(chǔ)
[userDefaults setObject:history forKey:@"ArticleBrowsingHistory"];
//讀取
NSArray *history = [userDefaults objectForKey:@"ArticleBrowsingHistory"];
所有用戶相對(duì)獨(dú)立,每個(gè)用戶是一個(gè)plist文件,在沙盒目錄/Library/Preferences/中,

其實(shí)這個(gè)超微型簡(jiǎn)單方便易用數(shù)據(jù)庫(kù)還可以實(shí)現(xiàn)好多功能,看你怎么操作了。如果你的App沒(méi)有后臺(tái),不同用戶的主題設(shè)置、語(yǔ)言設(shè)置可能不同,都可以通過(guò)這個(gè)來(lái)存儲(chǔ)。
總結(jié)
其實(shí)很多我們很常用的東西都有很多我們未發(fā)掘出的用途,生活也是同樣,學(xué)會(huì)發(fā)現(xiàn),一切會(huì)更美好。