归档是一个数据持久化的过程,该过程用某种格式来保存一个或多个对象,以便以后还原这些对象。
可以使用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指向的对象。