數據存儲之歸檔解檔 NSKeyedArchiver NSKeyedUnarchiver

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

1. 使用NSKeyedArchiver

如果要將各種類型的對象存儲到文件中,而不僅僅是字符串、數組、字典類型,利用NSKeyedArchiver類創建帶健(keyed)的檔案來完成將非常靈活。

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

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

KeyedArchiverBinaryData.png

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

下面我們結合代碼來學習歸檔與解檔:

創建Single View Application模板的demo,demo名稱為KeyedArchiver。在storyboard中添加四個UILabel、四個UITextField和兩個UIButton。布局如下:

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

歡迎更多指正:https://github.com/pro648/tips/wiki

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容