在構建應用程序時,有一個重要的問題是如何在每次啟動之間持久化數據,以便重現最后一次關閉應用前的狀態。在iOS和OS X上,蘋果提供了三種選擇:Core Data、屬性列表(Property List)和帶鍵值的編碼(NSKeyedArchiver)。當涉及到建模、查詢、遍歷、持久化等復雜的對象圖時,Core Data無可替代。但并非所有應用程序都需要查詢數據、處理復雜對象圖,有時候使用NSKeyedArchiver
更為簡單。
1. 使用NSKeyedArchiver
如果要將各種類型的對象存儲到文件中,而不僅僅是字符串、數組、字典類型,利用NSKeyedArchiver
類創建帶健(keyed)的檔案來完成將非常靈活。
在帶健的檔案中,會為每個歸檔對象提供一個名稱,即健(key)。根據這個key可以從歸檔中檢索該對象。這樣,就可以按照任意順序將對象寫入歸檔并進行檢索。另外,如果向類中添加了新的實例變量或刪除了實例變量,程序也可以進行處理。
NSKeyedArchiver
存儲在硬盤上的數據是二進制格式:
你可以通過文本編輯器打開二進制文件,但一般來說沒有必要。二進制文件是為計算機而設計,比純文本文件占用磁盤空間小,并且加載速度也更快。例如,Interface Builder通常以二進制格式存儲NIB文件。
下面我們結合代碼來學習歸檔與解檔:
創建Single View Application模板的demo,demo名稱為KeyedArchiver。在storyboard中添加四個UILabel
、四個UITextField
和兩個UIButton
。布局如下:
當點擊Archive按鈕時,把Name和Age對應的文本框內容歸檔到/Library/Application Support
內的文件夾。當點擊Unarchiver按鈕時,把剛創建歸檔程序讀入執行程序中,并對應的顯示到下面的兩個文本框中。
拖拽文本框IBOutlet屬性到ViewController.m
接口部分,拖轉兩個UIButton
的IBAction到實現部分,分別命名為archiver:
、unarchiver:
。完成后如下:
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextField *nameArchiver;
@property (weak, nonatomic) IBOutlet UITextField *ageArchiver;
@property (weak, nonatomic) IBOutlet UITextField *nameUnarchiver;
@property (weak, nonatomic) IBOutlet UITextField *ageUnarchiver;
@end
- (IBAction)archiver:(UIButton *)sender {
}
- (IBAction)unarchiver:(UIButton *)sender {
}
在聲明部分添加一個NSString
類型的documentsPath
對象,并使用懶加載初始化。該對象為沙盒中Documents\Application Support\
目錄。這樣只需要獲取一次路徑就可以重復使用,有助于提高性能。
@interface ViewController ()
...
@property (strong, nonatomic) NSString *documentsPath;
@end
- (NSString *)documentsPath {
if (!_documentsPath) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
if (paths.count > 0) {
_documentsPath = paths.firstObject;
// 如果目錄不存在,則創建該目錄。
if (! [[NSFileManager defaultManager] fileExistsAtPath:_documentsPath]) {
NSError *error;
// 創建該目錄
if(! [[NSFileManager defaultManager] createDirectoryAtPath:_documentsPath withIntermediateDirectories:YES attributes:nil error:&error])
{
NSLog(@"Failed to create directory. error: %@",error);
}
}
}
}
return _documentsPath;
}
在初始化documentsPath
時,使用NSSearchPathForDirectoriesInDomain()
方法獲取Library/Application Support/
目錄,如果目錄不存在,則創建該目錄。
對于NSString
、NSArray
、NSDictionary
、NSSet
、NSDate
、NSNumber
和NSData
之類的基本Objective-C類對象,都可以直接使用NSKeyedArchiver
歸檔和NSKeyedUnarchiver
讀取歸檔文件。
更新archiver:
方法,當點擊Archiver按鈕時對nameArchiver
和ageArchiver
中的文本進行歸檔。
- (IBAction)archiver:(UIButton *)sender {
// A 使用archiveRootObject: toFile: 方法歸檔
// 1.修改當前目錄為self.documentsPath
NSFileManager *sharedFM = [NSFileManager defaultManager];
[sharedFM changeCurrentDirectoryPath:self.documentsPath];
// 2.歸檔
if (![NSKeyedArchiver archiveRootObject:self.nameArchiver.text toFile:@"nameArchiver"]) {
NSLog(@"Failed to archive nameArchiver");
}
if (![NSKeyedArchiver archiveRootObject:self.ageArchiver.text toFile:@"ageArchiver"]) {
NSLog(@"Failed to archive ageArchiver");
}
}
上述代碼分步說明如下:
- 使用
NSFileManager
修改當前工作目錄為self.documentsPath
。 - 使用
archiveRootObject: toFile:
方法將文本框中的文本進行歸檔,該方法返回值為BOOL類型,歸檔成功返回YES
,歸檔失敗返回NO
。這里的toFile:
參數@"nameArchiver"
和@"ageArchiver"
均為相對路徑,相對于1中設定的當前路徑。
這篇文章會多次用到文件系統和
NSFileManager
,如果你還不熟悉,可以查看我的另一篇文章:使用NSFileManager管理文件系統。
再更新unarchiver:
方法,當點擊Unarchiver按鈕時讀取歸檔文件,并對應地顯示到nameUnarchiver
和ageUnarchiver
中。
- (IBAction)unarchiver:(UIButton *)sender {
// A 使用unarchiveObjectWithFile: 讀取歸檔
// 1.獲取歸檔路徑
NSString *nameArchiver = [self.documentsPath stringByAppendingPathComponent:@"nameArchiver"];
NSString *ageArchiver = [self.documentsPath stringByAppendingPathComponent:@"ageArchiver"];
// 2.讀取歸檔,并將其顯示在對應文本框。
self.nameUnarchiver.text = [NSKeyedUnarchiver unarchiveObjectWithFile:nameArchiver];
self.ageUnarchiver.text = [NSKeyedUnarchiver unarchiveObjectWithFile:ageArchiver];
}
上述代碼的分步說明如下:
- 使用
stringByAppendingPathComponent:
方法獲取歸檔路徑,這里也可以使用歸檔方法中設置當前路徑的方法,兩種方法效果一樣。 - 使用
NSKeyedUnarchiver
類的unarchiverObjectWithFile:
方法從路徑中讀取歸檔,并賦值給對應文本框。
運行demo,在上面兩個UITextField
中輸入文本,點擊Archiver按鈕即可把文本框中的文本歸檔。點擊Unarchiver按鈕即可讀取歸檔數據,并將其顯示到對應文本框。
2. 編碼方法和解碼方法
前面我們說過,對于NSString
、NSArray
等基本的Objective-C類對象,都可以直接使用NSKeyedArchiver
和NSKeyedUnarchiver
進行歸檔和解檔。而對于其他類型的對象,則必須告知系統如何編碼你的對象,以及如何解碼。這時你的類必須遵守NSCoding
協議,該協議只有兩個必須實現的方法encodeWithCoder:
和initWithCoder:
。
為遵守面向對象的設計原則,被編碼、解碼的對象負責對其實例變量進行編碼和解碼。編碼器通過調用encodeWithCoder:
和initWithCoder:
方法指導對象編碼、解碼其實例。encodeWithCoder:
指導對象編碼其實例變量至該方法參數中的編碼器,該方法可能會被調用多次;initWithCoder:
指導對象用參數中的數據初始化自身,它會替換任何其他初始化方法,并且每個對象僅發送一次。必須遵守NSCoding
協議、實現這兩個方法,該類才可以對其實例進行編碼、解碼。
繼續上面的demo,添加一個模版為Cocoa Touch Class,類名為Person,父類為NSObject
的文件。
在Person.h
中添加以下屬性和方法:
@interface Person : NSObject
@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age;
- (void)setName:(NSString *)name age:(NSInteger)age;
@end
在Person.m
實現setName: age:
方法。
- (void)setName:(NSString *)name age:(NSInteger)age {
self.name = name;
self.age = age;
}
下面是解碼方法和編碼方法。
// 1.編碼方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"PersonName"];
[aCoder encodeInteger:self.age forKey:@"PersonAge"];
}
// 2.解碼方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"PersonName"];
self.age = [aDecoder decodeIntegerForKey:@"PersonAge"];
}
return self;
}
上述代碼的分步說明如下:
- 該程序向編碼方法
encodeWithCoder:
傳入一個NSCoder
對象作為參數。由于Person
類直接繼承自NSObject
,所以無需擔心編碼繼承的實例變量。如果的確擔心,并且知道類的父類符合NSCoding
協議,那么在編碼方法開始處添加[super encodeWithCoder: encoder]
,確保繼承的實例變量也被編碼。另外,不同類型對象使用不同編碼方法。如果編碼NSString
類型對象,使用encodeObject: forKey:
方法,如果編碼NSInteger
類型對象,使用encodeInteger: forKey:
方法。這里的鍵名是任意的,只要跟解碼時的一致即可。為防止子類和父類使用相同鍵而導致沖突,可以像這里定義的一樣,制定鍵名時將類名放在鍵名前加以區分。 - 解碼過程與編碼剛好相反。傳遞給
initWithCoder:
的參數也是NSCoder
對象,不用擔心這個參數,只要記住它是想要從歸檔中提取的對象即可。如果擔心解碼繼承的實例變量,且該父類遵守NSCoding
協議,可以用self = [super initWithCoder: decoder];
開始解碼方法。只要鍵與編碼時相同就可以解碼。
進入ViewController.m
方法,導入Person.h
,并在接頭部分添加以下屬性:
@interface ViewController ()
...
@property (strong, nonatomic) Person *person;
@end
最后記得在viewDidLoad
方法中初始化該方法。
注釋掉archiver:
方法內代碼,并添加以下代碼,以便歸檔Person
類。
- (IBAction)archiver:(UIButton *)sender {
// B 使用initForWritingWithData: 歸檔。
// 1.把當前文本框內文本傳送給person。
[self.person setName:self.nameArchiver.text age:[self.ageArchiver.text integerValue]];
// 2.使用initForWritingWithMutableData: 方法歸檔內容至mutableData。
NSMutableData *mutableData = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:mutableData];
[archiver encodeObject:self.person forKey:@"person"];
[archiver finishEncoding];
// 3.把歸檔寫入Library/Application Support/Data目錄。
NSString *filePath = [self.documentsPath stringByAppendingPathComponent:@"Data"];
if (![mutableData writeToFile:filePath atomically:YES]) {
NSLog(@"Failed to write file to filePath");
}
}
上述代碼的分布說明如下:
- 把當前文本框內文本傳送給
person
。記得在`viewDidLoad - 先創建一個空緩沖區,其大小將隨著程序執行的需要而擴展。通過
intiForWritingWithMutableData:
方法以指定歸檔數據的存儲空間為mutableData
,現在可以向archiver
對象發送編碼消息,以便歸檔對象,這里可以歸檔多個對象。所有對象都歸檔后必須向archiver
發送finishEncoding
消息。在此之后,就不能編碼其他對象了。此時,你預留的mutableData
區域包含歸檔對象。 - 使用
writeToFile: atomically:
方法把歸檔后的對象寫入文件,該方法返回值為BOOL類型,寫入成功時返回YES
;操作失敗時返回NO
。atomically:
參數為YES
表示希望首先將文件寫入到臨時備份中,且一旦成功,將把該備份重命名為指定目錄名。這是一種安全措施,可以避免文件在操作過程中因系統崩潰而致使原文件、新文件均損壞。如果參數為NO
,則會直接在指定目錄寫入文件。
同樣,注釋掉unarchiver:
中原來代碼,并添加以下代碼讀取歸檔。
- (IBAction)unarchiver:(UIButton *)sender {
...
// B 使用initForReadingWithData: 讀取歸檔。
// 1.從Library/Application Support/Data目錄獲取歸檔文件。
NSString *filePath = [self.documentsPath stringByAppendingPathComponent:@"Data"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
// 2.使用initForReadingWithData: 讀取歸檔。
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
self.person = [unarchiver decodeObjectForKey:@"person"];
[unarchiver finishDecoding];
// 3.把讀取到的內容顯示到對應文本框。
self.nameUnarchiver.text = self.person.name;
self.ageUnarchiver.text = [@(self.person.age) stringValue];
}
讀取歸檔時,首先通過dataWithContentsOfFile:
方法獲取歸檔文件,之后使用initForReadingWithData:
讀取歸檔,在解碼結束時,一定要向unarchiver
發送finishDecoding
消息結束解碼。最后將讀取到的歸檔內容顯示到對應文本框。
運行app,可以像之前一樣對文本框內容進行歸檔、解檔。
如果想要了解屬性列表及通過代碼練習,可以查看這篇文章:使用偏好設置、屬性列表、歸檔解檔保存數據、恢復數據。
你也可以嘗試注釋掉
Person.m
中的編碼方法和解碼方法再次運行demo,點擊按鈕時會在控制臺看到出錯消息。
你也可以通過添加觀察者,在應用程序進入后臺時(通知為UIApplicationDidEnterBackgroundNotifiaction)歸檔文件,這樣即使app被終止,數據也不會丟失。你可以自行完成,如果遇到問題,可以通過文章底部網址獲取源碼查看。
3. 使用歸檔程序復制對象
可以使用歸檔功能實現深復制,可以將對象歸檔到一個緩沖區,然后把它從緩沖區解歸檔,這樣就實現了深復制。如下所示:
NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"one"], [NSMutableString stringWithString:@"two"], nil];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mutableArray];
NSMutableArray *mutableArray2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
如果你對其是否進行了深復制有疑惑,可以通過修改其中一個數組的元素,查看另一個數組內元素是否改變來驗證。
深入了解深復制、淺復制,查看深復制、淺復制、copy、mutableCopy這篇文章。
4. 三種歸檔方法的區別
在這篇文章中使用了archiveRootObject: toFile:
、initForWritingWithMutableData:
和archivedDataWithRootObject:
三種類型歸檔方法,它們區別如下:
-
archiveRootObject: toFile:
不能決定如何處理歸檔的數據,直接被寫入了文件。 -
initForWritingWithMutableData:
歸檔的數據可以通過網絡分發,除此之外還可以把多個對象歸檔到一個緩沖區。 -
archivedDataWithRootObject:
這種方法歸檔的數據可以通過網絡分發,非常靈活。
總之,只是方便與靈活的區別。
Demo名稱:KeyedArchiver
源碼地址:https://github.com/pro648/BasicDemos-iOS
參考資料: