數據存儲之歸檔解檔 NSKeyedArchiver NSKeyedUnarchiver

在構建應用程序時,有一個重要的問題是如何在每次啓動之間持久化數據,以便重現最後一次關閉應用前的狀態。在iOS和OS X上,蘋果提供了三種選擇:Core Data、屬性列表(Property List)和帶鍵值的編碼(NSKeyedArchiver)。當涉及到建模、查詢、遍歷、持久化等複雜的對象圖時,Core Data無可替代。但並不是全部應用程序都須要查詢數據、處理複雜對象圖,有時候使用NSKeyedArchiver更爲簡單。php

1. 使用NSKeyedArchiver

若是要將各類類型的對象存儲到文件中,而不只僅是字符串、數組、字典類型,利用NSKeyedArchiver類建立帶健(keyed)的檔案來完成將很是靈活。git

在帶健的檔案中,會爲每一個歸檔對象提供一個名稱,即健(key)。根據這個key能夠從歸檔中檢索該對象。這樣,就能夠按照任意順序將對象寫入歸檔並進行檢索。另外,若是向類中添加了新的實例變量或刪除了實例變量,程序也能夠進行處理。github

NSKeyedArchiver存儲在硬盤上的數據是二進制格式:數組

 
KeyedArchiverBinaryData.png

你能夠經過文本編輯器打開二進制文件,但通常來講沒有必要。二進制文件是爲計算機而設計,比純文本文件佔用磁盤空間小,而且加載速度也更快。例如,Interface Builder一般以二進制格式存儲NIB文件。安全

下面咱們結合代碼來學習歸檔與解檔:網絡

建立Single View Application模板的demo,demo名稱爲KeyedArchiver。在storyboard中添加四個UILabel、四個UITextField和兩個UIButton。佈局以下:app

 
KeyedArchiverStoryboard.png

當點擊Archive按鈕時,把NameAge對應的文本框內容歸檔到/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/目錄,若是目錄不存在,則建立該目錄。

對於NSStringNSArrayNSDictionaryNSSetNSDateNSNumberNSData之類的基本Objective-C類對象,均可以直接使用NSKeyedArchiver歸檔和NSKeyedUnarchiver讀取歸檔文件。

更新archiver:方法,當點擊Archiver按鈕時對nameArchiverageArchiver中的文本進行歸檔。

- (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"); } } 

上述代碼分步說明以下:

  1. 使用NSFileManager修改當前工做目錄爲self.documentsPath
  2. 使用archiveRootObject: toFile:方法將文本框中的文本進行歸檔,該方法返回值爲BOOL類型,歸檔成功返回YES,歸檔失敗返回NO。這裏的toFile:參數@"nameArchiver"@"ageArchiver"均爲相對路徑,相對於1中設定的當前路徑。

這篇文章會屢次用到文件系統和NSFileManager,若是你還不熟悉,能夠查看個人另外一篇文章:使用NSFileManager管理文件系統

再更新unarchiver:方法,當點擊Unarchiver按鈕時讀取歸檔文件,並對應地顯示到nameUnarchiverageUnarchiver中。

- (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]; } 

上述代碼的分步說明以下:

  1. 使用stringByAppendingPathComponent:方法獲取歸檔路徑,這裏也可使用歸檔方法中設置當前路徑的方法,兩種方法效果同樣。
  2. 使用NSKeyedUnarchiver類的unarchiverObjectWithFile:方法從路徑中讀取歸檔,並賦值給對應文本框。

運行demo,在上面兩個UITextField中輸入文本,點擊Archiver按鈕便可把文本框中的文本歸檔。點擊Unarchiver按鈕便可讀取歸檔數據,並將其顯示到對應文本框。

 
KeyedArchiverA.gif

2. 編碼方法和解碼方法

前面咱們說過,對於NSStringNSArray等基本的Objective-C類對象,均可以直接使用NSKeyedArchiverNSKeyedUnarchiver進行歸檔和解檔。而對於其餘類型的對象,則必須告知系統如何編碼你的對象,以及如何解碼。這時你的類必須遵照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; } 

上述代碼的分步說明以下:

  1. 該程序向編碼方法encodeWithCoder:傳入一個NSCoder對象做爲參數。因爲Person類直接繼承自NSObject,因此無需擔憂編碼繼承的實例變量。若是的確擔憂,而且知道類的父類符合NSCoding協議,那麼在編碼方法開始處添加[super encodeWithCoder: encoder],確保繼承的實例變量也被編碼。另外,不一樣類型對象使用不一樣編碼方法。若是編碼NSString類型對象,使用encodeObject: forKey:方法,若是編碼NSInteger類型對象,使用encodeInteger: forKey:方法。這裏的鍵名是任意的,只要跟解碼時的一致便可。爲防止子類和父類使用相同鍵而致使衝突,能夠像這裏定義的同樣,制定鍵名時將類名放在鍵名前加以區分。
  2. 解碼過程與編碼恰好相反。傳遞給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"); } } 

上述代碼的分佈說明以下:

  1. 把當前文本框內文本傳送給person。記得在`viewDidLoad
  2. 先建立一個空緩衝區,其大小將隨着程序執行的須要而擴展。經過intiForWritingWithMutableData:方法以指定歸檔數據的存儲空間爲mutableData,如今能夠向archiver對象發送編碼消息,以便歸檔對象,這裏能夠歸檔多個對象。全部對象都歸檔後必須向archiver發送finishEncoding消息。在此以後,就不能編碼其餘對象了。此時,你預留的mutableData區域包含歸檔對象。
  3. 使用writeToFile: atomically:方法把歸檔後的對象寫入文件,該方法返回值爲BOOL類型,寫入成功時返回YES;操做失敗時返回NOatomically:參數爲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:三種類型歸檔方法,它們區別以下:

  1. archiveRootObject: toFile:不能決定如何處理歸檔的數據,直接被寫入了文件。
  2. initForWritingWithMutableData:歸檔的數據能夠經過網絡分發,除此以外還能夠把多個對象歸檔到一個緩衝區。
  3. archivedDataWithRootObject:這種方法歸檔的數據能夠經過網絡分發,很是靈活。

總之,只是方便與靈活的區別。

Demo名稱:KeyedArchiver
源碼地址:https://github.com/pro648/BasicDemos-iOS

參考資料:

  1. NSCoding / NSKeyed​Archiver
  2. Differences with archiveRootObject:toFile: and writeToFile:
  3. Saving Application Data



做者:pro648來源:簡書

相關文章
相關標籤/搜索