在構建應用程序時,有一個重要的問題是如何在每次啓動之間持久化數據,以便重現最後一次關閉應用前的狀態。在iOS和OS X上,蘋果提供了三種選擇:Core Data、屬性列表(Property List)和帶鍵值的編碼(NSKeyedArchiver)。當涉及到建模、查詢、遍歷、持久化等複雜的對象圖時,Core Data無可替代。但並不是全部應用程序都須要查詢數據、處理複雜對象圖,有時候使用NSKeyedArchiver
更爲簡單。php
若是要將各類類型的對象存儲到文件中,而不只僅是字符串、數組、字典類型,利用NSKeyedArchiver
類建立帶健(keyed)的檔案來完成將很是靈活。git
在帶健的檔案中,會爲每一個歸檔對象提供一個名稱,即健(key)。根據這個key能夠從歸檔中檢索該對象。這樣,就能夠按照任意順序將對象寫入歸檔並進行檢索。另外,若是向類中添加了新的實例變量或刪除了實例變量,程序也能夠進行處理。github
NSKeyedArchiver
存儲在硬盤上的數據是二進制格式:數組
你能夠經過文本編輯器打開二進制文件,但通常來講沒有必要。二進制文件是爲計算機而設計,比純文本文件佔用磁盤空間小,而且加載速度也更快。例如,Interface Builder一般以二進制格式存儲NIB文件。安全
下面咱們結合代碼來學習歸檔與解檔:網絡
建立Single View Application模板的demo,demo名稱爲KeyedArchiver。在storyboard中添加四個UILabel
、四個UITextField
和兩個UIButton
。佈局以下:app
當點擊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按鈕便可讀取歸檔數據,並將其顯示到對應文本框。
前面咱們說過,對於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
。記得在`viewDidLoadintiForWritingWithMutableData:
方法以指定歸檔數據的存儲空間爲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被終止,數據也不會丟失。你能夠自行完成,若是遇到問題,能夠經過文章底部網址獲取源碼查看。
可使用歸檔功能實現深複製,能夠將對象歸檔到一個緩衝區,而後把它從緩衝區解歸檔,這樣就實現了深複製。以下所示:
NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"one"], [NSMutableString stringWithString:@"two"], nil]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mutableArray]; NSMutableArray *mutableArray2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
若是你對其是否進行了深複製有疑惑,能夠經過修改其中一個數組的元素,查看另外一個數組內元素是否改變來驗證。
深刻了解深複製、淺複製,查看深複製、淺複製、copy、mutableCopy這篇文章。
在這篇文章中使用了archiveRootObject: toFile:
、initForWritingWithMutableData:
和archivedDataWithRootObject:
三種類型歸檔方法,它們區別以下:
archiveRootObject: toFile:
不能決定如何處理歸檔的數據,直接被寫入了文件。initForWritingWithMutableData:
歸檔的數據能夠經過網絡分發,除此以外還能夠把多個對象歸檔到一個緩衝區。archivedDataWithRootObject:
這種方法歸檔的數據能夠經過網絡分發,很是靈活。總之,只是方便與靈活的區別。
Demo名稱:KeyedArchiver
源碼地址:https://github.com/pro648/BasicDemos-iOS
參考資料:
做者:pro648來源:簡書