归档是一个数据持久化的过程,该过程用某种格式来保存一个或多个对象,以便以后还原这些对象。
可以使用NSKeyedArchiver类创建带键(keyed)的文件来完成。在带键的文件中,每个归档的对象对应一个键,从文件中加载对象时,就是根据这个键来检索对象。
1.归档基本的Foundation对象
使用NSKeyedArchiver可以归档和恢复NSString、NSArray、NSDictionary、NSSet、NSDate、NSNumber和NSData等基本的Foundation对象。代码如下:
- (IBAction)archiveObject:(id)sender { // 创建Foundation对象 NSString *str = @"NSString Object"; NSDate *date = [NSDate date]; NSNumber *number = [NSNumber numberWithInt:100]; NSArray *array = [NSArray arrayWithObjects:str, date, number, nil]; NSDictionary *dictionary = [NSDictionary dictionaryWithObjects:@[str, date, number] forKeys:@[@"NSString Key", @"NSDate Key", @"NSNumber Key"]]; // 获取文件路径 NSString *documentPath = [self getDocumentPath]; NSString *arrayFilePath = [documentPath stringByAppendingPathComponent:@"array.plist"]; NSString *dictionaryFilePath = [documentPath stringByAppendingPathComponent:@"dictionary.plist"]; // 归档数组和字典对象 [NSKeyedArchiver archiveRootObject:array toFile:arrayFilePath]; [NSKeyedArchiver archiveRootObject:dictionary toFile:dictionaryFilePath]; } - (IBAction)unarchiveObject:(id)sender { // 获取文件路径 NSString *documentPath = [self getDocumentPath]; NSString *arrayFilePath = [documentPath stringByAppendingPathComponent:@"array.plist"]; NSString *dictionaryFilePath = [documentPath stringByAppendingPathComponent:@"dictionary.plist"]; // 恢复对象 NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:arrayFilePath]; NSDictionary *dictionary = [NSKeyedUnarchiver unarchiveObjectWithFile:dictionaryFilePath]; // 输出对象信息 NSString *dataInfo = [NSString stringWithFormat:@"array = %@\ndictionary = %@", array.description, dictionary.description]; self.showObjects_textView.text = dataInfo; } /* 获取Documents文件夹路径 */ - (NSString *)getDocumentPath { NSArray *documents = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentPath = documents[0]; return documentPath; }
点击视图中的Archive按钮,将包含NSString,NSNumber和NSDate对象的数组或字典对象归档到文件中,在Documents目录下可以看到array.plist和dictionary.plist文件:
再点击视图中的Unarchive按钮,首先从文件中恢复数组和字典对象,然后在视图中显示其信息:
要特别注意的是,该方法只能应用于基本的Foundation对象,如果NSArray或NSDictionary中包含自定义的类对象,或直接归档自定义类对象,那么程序将会崩溃:
Book类:
#import <Foundation/Foundation.h> @interface Book : NSObject @property (strong, nonatomic) NSString *name; @property (assign, nonatomic) BOOL published; @property (assign, nonatomic) float price; @property (strong, nonatomic) NSMutableArray *info; @end
// 创建自定义对象 Book *book = [[Book alloc] init]; book.name = @"2.0"; book.published = NO; book.price = 100.0; book.info = [[NSMutableArray alloc] init]; [book.info addObject:@"Programming in Objective-C Fourth Edition"]; NSArray *array = [NSArray arrayWithObjects:str, date, number, book, nil];
2014-02-01 14:05:52.157 KeyedArchiver[1126:70b] -[Book encodeWithCoder:]: unrecognized selector sent to instance 0xcb4e560 2014-02-01 14:05:52.161 KeyedArchiver[1126:70b] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException‘, reason: ‘-[Book encodeWithCoder:]: unrecognized selector sent to instance 0xcb4e560‘
原因是Book类的encodeWithCoder方法没有实现。
2.归档和恢复自定义类对象
对于自定义的类必须要实现<NSCoding>协议中的encodeWithCoder和initWithCoder方法,才能归档和恢复这个类产生的对象。
#import "Book.h" static NSString *kBookName = @"BookName"; static NSString *kPublished = @"BookPublished"; static NSString *kPrice = @"BookPrice"; static NSString *kInfo = @"BookInfo"; @interface Book () <NSCoding> @end @implementation Book - (NSString *)description { return [NSString stringWithFormat:@"BookName = %@ Published = %i Price = %f BookInfo = %@", self.name, self.published, self.price, self.info]; } #pragma mark - NSCoding Delegate - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.name forKey:kBookName]; [aCoder encodeBool:self.published forKey:kPublished]; [aCoder encodeFloat:self.price forKey:kPrice]; [aCoder encodeObject:self.info forKey:kInfo]; } - (id)initWithCoder:(NSCoder *)aDecoder { self.name = [aDecoder decodeObjectForKey:kBookName]; self.published = [aDecoder decodeBoolForKey:kPublished]; self.price = [aDecoder decodeFloatForKey:kPrice]; self.info = [aDecoder decodeObjectForKey:kInfo]; return self; } @end
运行程序,先Archive,再Unarchive,结果如下:
如果要归档Book类的子类对象,那么要在encodeWithCoder和decodeWithCoder方法中首先调用父类的编码和解码方法。这里要注意,如果在Book类中<NSCoding>协议声明在匿名类别中,那么协议的方法将是私有的,子类无法访问,因此要将<NSCoding>协议声明移到接口部分:
@interface Book () // <NSCoding> @end修改为:
@interface Book : NSObject <NSCoding>
下面新建一个Book类的子类Literature类:
#import "Book.h" @interface Literature : Book <NSCoding> @property (copy, nonatomic) NSString *author; @end
#import "Literature.h" static NSString *kAuthor = @"LiteratureAuthor"; @implementation Literature - (NSString *)description { NSString *bookDes = [super description]; NSString *literDes = [bookDes stringByAppendingFormat:@"Author = %@", self.author]; return literDes; } #pragma mark - NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; [aCoder encodeObject:self.author forKey:kAuthor]; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; self.author = [aDecoder decodeObjectForKey:kAuthor]; return self; } @end在archive的action方法中加入Literature类对象,并进行归档:
// 创建Book子类对象 Literature *literature = [[Literature alloc] init]; literature.author = @"Shakespeare"; literature.name = book.name; literature.published = book.published; literature.price = book.price; literature.info = [[NSMutableArray alloc] initWithArray:[book.info copy]]; NSArray *array = [NSArray arrayWithObjects:str, date, number, book, literature, nil];
运行结果:
注意:
(1)在归档时类的键可以任意指定,为了避免各个类的键出现重名,所以最好在键名前面加上类名前缀,例如:
Book类:
static NSString *kBookName = @"BookName"; static NSString *kPublished = @"BookPublished"; static NSString *kPrice = @"BookPrice"; static NSString *kInfo = @"BookInfo";
Literature类:
static NSString *kAuthor = @"LiteratureAuthor";
(2)archive方法会返回一个BOOL值:
+ (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path;
通过BOOL可以判断归档是否成功。
unarchive方法返回的是对象的引用,可以判断其是否为nil从而保证程序的健壮性:
+ (id)unarchiveObjectWithFile:(NSString *)path;
更加安全的做法是:
// 归档数组和字典对象 if (![NSKeyedArchiver archiveRootObject:array toFile:arrayFilePath]) { NSLog(@"fail to archive array object"); } if (![NSKeyedArchiver archiveRootObject:dictionary toFile:dictionaryFilePath]) { NSLog(@"fail to archive dictionary object"); }
// 恢复对象 NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:arrayFilePath]; NSDictionary *dictionary = [NSKeyedUnarchiver unarchiveObjectWithFile:dictionaryFilePath]; // 输出对象信息 if (array && dictionary) { NSString *dataInfo = [NSString stringWithFormat:@"array = %@\ndictionary = %@", array.description, dictionary.description]; self.showObjects_textView.text = dataInfo; }
3.使用NSData类归档自定义对象
如果我们不希望创建数组或字典等集合来保存对象,并且想单独保存一个或几个对象到同一个文件,那么我们可以先在内存中申请一块内存空间用来保存NSData对象,然后使用NSKeyedArchiver类将这些对象编码成NSData类并保存到内存中,在编码完成后将这些二进制数据写入磁盘的文件中。
在恢复对象时,其过程是相反的。
归档的代码:
// 创建自定义对象 Book *book = [[Book alloc] init]; book.name = @"OC 2.0"; book.published = NO; book.price = 100.0; book.info = [[NSMutableArray alloc] init]; [book.info addObject:@"Programming in Objective-C Fourth Edition"]; // 创建Book子类对象 Literature *literature = [[Literature alloc] init]; literature.author = @"Shakespeare"; literature.name = book.name; literature.published = book.published; literature.price = book.price; literature.info = [[NSMutableArray alloc] initWithArray:[book.info copy]]; // 将Book类和Literature类编码成二进制数据 NSMutableData *tempData = [NSMutableData data]; NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:tempData]; [archiver encodeObject:book forKey:@"Book"]; [archiver encodeObject:literature forKey:@"Literature"]; [archiver finishEncoding]; // 保存编码后的数据 NSString *documentPath = [self getDocumentPath]; NSString *dataFilePath = [documentPath stringByAppendingPathComponent:@"data.plist"]; if (![tempData writeToFile:dataFilePath atomically:YES]) { NSLog(@"fail to archive data object"); }
恢复对象的代码:
// 获取文件路径 NSString *documentPath = [self getDocumentPath]; NSString *dataFilePath = [documentPath stringByAppendingPathComponent:@"data.plist"]; // 从文件中读取二进制数据 NSData *tempData = [NSData dataWithContentsOfFile:dataFilePath]; if (tempData) { // 通过解码恢复对象 NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:tempData]; Book *book = [unarchiver decodeObjectForKey:@"Book"]; Literature *literature = [unarchiver decodeObjectForKey:@"Literature"]; [unarchiver finishDecoding]; // 显示对象内容 self.showObjects_textView.text = [NSString stringWithFormat:@"Book = %@, Literature = %@", book, literature]; }
4.使用NSKeyedArchiver类压缩单个对象
如果只是保存一个对象,可以先将一个对象压缩成二进制数据,调用以下方法:
+ (NSData *)archivedDataWithRootObject:(id)rootObject;
然后用NSData的writeToFile方法即可:
- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
另外,通过压缩和解压可以实现对象的深复制:
NSMutableArray *marray1 = [[NSMutableArray alloc] initWithObjects: [NSMutableString stringWithString:@"1"], [NSMutableString stringWithString:@"2"], [NSMutableString stringWithString:@"3"], nil]; NSMutableArray *marray2 = nil; NSData *tempData = [NSKeyedArchiver archivedDataWithRootObject:marray1]; marray2 = [NSKeyedUnarchiver unarchiveObjectWithData:tempData]; NSMutableString *mstr = marray1[0]; [mstr appendString:@" append"]; NSLog(@"marray1 = %@", marray1); NSLog(@"marray2 = %@", marray2);
2014-02-01 16:22:22.531 DeepCopy[2022:303] marray1 = ( "1 append", 2, 3 ) 2014-02-01 16:22:22.533 DeepCopy[2022:303] marray2 = ( 1, 2, 3 )
由输出结果可以看到复制的不是对象的引用,而是对象的具体内容。所以对marray1指向的对象的修改不会影响到marray2指向的对象。