归档(archiving)将对象与数值储存至架构独立(architecture-independent)的位串流,它能储存至文件待日后复原,也能直接传送给另一个程序使用,进而让对象与数值的关系在任何程序中,都具备一致性。归档让相关的对象与键值的集合,能以更详细的方式记录。
一、Property LIst 结构表示方法
Property List(通称为plist)是一个对象间结构的表示方法。在 Mac OS X 的系统中,应用程序能将 property lists 输出为二进制(binary)格式或 XML 格式。plist 具备轻易建立与修改的特性,用户仅需使用文本编辑器,或在 Mac OS X 开发工具中的 Property List Editor 应用程序(位于 Macintosh HD/Deveploer/Application/Utilities 目录)进行编写即可。Foundation Framework 的集合对象 NSArray、NSDictionary,两者都有方法能直接将内容以 XML plist 的格式记录于文件。
范例程序 archive_property_list_write
// archive_property_list_write
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 文件储存路径
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"dict.plist"];
// 建立 dictionary,放入 key-pair 键值
NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:
@"printf()", @"C",
@"cout<<", @"C++",
@"NSLog()", @"Objective-C",
@"System.out.print()", @"Java", nil];
// 将 dictionary 写入文件 path
if (YES == [dict writeToFile:path atomically:YES])
{
NSLog(@"文件写入成功");
}
}
return 0;
}
程序先以 stringByAppendingPathComponent: 方法,定义 NSDictionary 的储存文件的路径。之后以 writeToFile:atomically: 方法,将数据写入文件中。请注意键值必须是 NSString 类型,才能成功写入文件。以下为输出的文件内容:
输出结果
C
printf()
C++
cout<<
Java
System.out.print()
Objective-C
NSLog()
能看见这是一个标准的 XML 格式文件,除了键值
数据类型 | XML 元素 | Foundation framework 类别 |
---|---|---|
array | NSArray | |
dictionary | NSDictionary | |
string | NSString | |
data | NSData | |
date | NSDate | |
number-integer | NSNumber(intValue) | |
number-floating point | NSNumber(intFloat) | |
Boolean | NSNumber(boolValue) |
表1中的数据类型,称为 plist 对象(property list object)类型,也就是 plist 结构所支持的对象类型。范例程序 archive_property_list_read 能将 XML plist 文件还原至程序。
范例程序 archive_property_list_read
// archive_property_list_read
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 文件储存路径
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"dict.plist"];
// 将文件读取至 recovery,然后显示出来
NSDictionary * recovery = [NSDictionary dictionaryWithContentsOfFile:path];
for (NSString *key in recovery) {
NSLog(@"key:%@, value:%@", key, [recovery objectForKey:key]);
}
}
return 0;
}
输出结果
key:Java, value:System.out.print()
key:Objective-C, value:NSLog()
key:C, value:printf()
key:C++, value:cout<<
程序由原路径将 XML plist 文件,利用 dictionaryWithContentsOfFile: 方法将数据读取,并指定给变量 recover,若这里将 NSDictionary 类别改成 NSArray,是一个错误的操作方式,对象依然能产生,但会返回 NULL 不会有任何内容,请特别注意。
二、NSKeyedArchiver类别
对象图形(object graphs)是指对象与对象间关系图。相较于上述介绍的对象,archiving 能处理较复杂的对象图形,以及更多 plist 结构不支持的对象,因而突显它的重要性。
NSCoder 是所有 archiving 对象的抽象父类别,它定义一些 archive 的必要方法,Foundation framework 底下有5种类别继承自它,如表2所示:
Archiving 特色 | 类别名称 | 功能 |
---|---|---|
顺序归档 Sequential archive | NSArchiver | 以顺序的方式进行编码 |
顺序归档 Sequential archive | NSUnarchiver | 以顺序的方式进行编码 |
键值归档 Keyed archive | NSKeyedArchiver | 以键值组的方式进行编码 |
键值归档 Keyed archive | NSKeyedUnarchiver | 以键值组的方式进行编码 |
分布式对象 Distributed objects | NSPortCoder | 程序或线程之间交换数据的方式 |
由于顺序归档已于 Mac OS X 10.2 退役,而分布式对象在一般程序中较少使用,因此本节着重于运用最广泛的 NSKeyedArchiver,它也是在 10.2 时出现的功能。范例程序 archive_nsKeyedArchiver_write 示范如何使用 NSKeyedArchiver 类别,将对象储存为文件。
范例程序 archive_nsKeyedArchiver_write
// archive_nsKeyedArchiver_write
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 文件储存路径
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"array.plist"];
// 创建一个数组
NSArray *array = [NSArray arrayWithObjects:@"one", @"two", @"three", nil];
// 写入文件
if (YES == [NSKeyedArchiver archiveRootObject:array toFile:path]) {
NSLog(@"文件写入成功");
}
}
return 0;
}
若文件 array.plist 成功建立,会输出“文件写入成功”的提示信息。由上述程序发现,以 NSKeyedArchiver 的类别方法 rchiveRootObject:toFile: 将数组 array 以 property list 的格式存入路径 path 指定的文件。
范例程序 archive_nsKeyedArchiver_read 将刚才储存至 array.plist 的内容,还原至目前程序中。
范例程序 archive_nsKeyedArchiver_read
// archive_nsKeyedArchiver_read
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 文件储存路径
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"array.plist"];
// 从文件读取数组
NSArray *recovery = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
// 显示数组
for (NSString *strTemp in recovery) {
NSLog(@"%@",strTemp);
}
}
return 0;
}
unarchiveObjectWithFile: 也是NSKeyedUnarchiver 的类别方法,只需要传入文件路径 path,即可取得复原的内容。请注意,返回值为 nil 代表文件不存在,若文件不是有效的 archive 内容,则会出现 NSInvalidArgumentException 的异常。
三、自定义类别的 archive
上述谈了关于 archive 的内容,也由表格了解了 archive 支持的基本数据类型,接着我们将探讨的主题是自定义类。若仔细查看官方的技术文件,能发现几乎所有 Foundation framework 的类别,都遵循 NSCoding 协议,并实现 encodeWithCoder: 以及 initWithCoder: 这二个必要的方法,它们都有一个 NSCoder 类型的参数,前者的参数能将 archive 的对象编码为串流,后者的参数能将串流译码为 unarchive 的对象。
archive 的过程对应的是 encodeWithCoder:,而 unarchive 则是对应 initWithCoder:。表3列出 archive 与 unarchive 时,对象(变量)类型对应的 NSCoder 方法。
对象(变量)类型 | 对应的编码方法 | 对应的译码方法 |
---|---|---|
Foundation Framework object | - encodeObject:forKey: | - decodeObjectForKey: |
Boolean | - encodeBool:forKey: | - decodeBoolForKey: |
int | - encodeInt:forKey: | - decodeIntForKey: |
float | - encodeFloat:forKey: | - decodeFloatForKey: |
double | - encodeDouble:forKey: | - decodeDoubleForKey: |
char, byte 与 CString | - encodeBytes:length:forKey: | - decodeBoolForKey: |
标准 C 语言中,基本变量类型的指针变量无法编码与译码,唯有字符数组能以 bytes 的方式进行,请读者特别注意。以下程序示范如何使用 NSCoding 协议的两个必要方法,制作一个支持 archive 的自定义类别。
范例程序 Subject.h
// archive_class_write/Subject.h
#import
@interface Subject : NSObject {
NSString *_name;
double _hours;
int _score;
}
@property NSString *name;
@property double hours;
@property int score;
- (void)setSubject:(NSString*)tempName
hours:(double)tempHours score:(int)tempScore;
- (void)print;
@end
范例程序 Subject.m
// archive_class_write/Subject.m
#import "Subject.h"
@implementation Subject
- (void)setSubject:(NSString*)tempName
hours:(double)tempHours score:(int)tempScore
{
_name = [tempName retain];
_hours = tempHours;
_score = tempScore;
}
- (void)print
{
NSLog(@"\nSubject:%@, Hours:%g, Score:%i", self.name, self.hours, self.score);
}
// 基本类型与对象,编码方式
- (void)encodeWithCoder:(NSCoder *)aCoder
{
// [super encodeWithCoder:aCoder];
[aCoder encodeObject:self.name forKey:@"SUBJECT_KEY"];
[aCoder encodeDouble:self.hours forKey:@"HOURS_KEY"];
[aCoder encodeInt:self.score forKey:@"SCORE_KEY"];
}
// 基本类型与对象,译码方式
- (id)initWithCoder:(NSCoder *)aDecoder
{
// self = [super initWithCoder:aDecoder];
self = [super init];
if (self) {
_name = [aDecoder decodeObjectForKey:@"SUBJECT_KEY"];
_hours = [aDecoder decodeDoubleForKey:@"HOURS_KEY"];
_score = [aDecoder decodeIntForKey:@"SCORE_KEY"];
}
return self;
}
@end
由上述程序了解,无论是何种类型对应的方法,都必须附上键值字符串,以上译码过程能以键值取得原来的内容。换言之,在使用 NSKeyedArchiver 时,会调用 encodeWithCoder: 方法,然后取得编码过的串流加以应用。相反的,在使用 NSKeyedUnarchiver 时,会调用 initWithCoder: 方法,将串流中的内容解码并存入对应的类别成员,最后再返回类别本身。若是根类已遵循 NSCoding 协议,并实现了这二个必要的方法,则 encodeWithCoder: 必须将下列语句插入至第一行:
[super encodeWithCoder:aCoder];
让根类别先行将它的类别成员编码至 aCoder,再接续由子类别进行此任务。同样的,initWithCoder:也必须将下列语句插入至第一行:
self = [super initWithCoder:aDecoder];
让根类别先将 aDecoder 内容中,自己的部分译码还原,再进行子类别的程序语句。这里也请注意,若是对象类型进行译码,必须自行 retain 对象,否则 aDecoder 回收时,也会将对象一并清除。以下为范例程序:
范例程序 archive_class_write
// archive_class_write
#import
#import "Subject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 储存路径
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"subject.plist"];
// 创建数组
NSMutableArray *SubArray = [[NSMutableArray alloc] init];
Subject * sub1 = [[Subject alloc] init];
Subject * sub2 = [[Subject alloc] init];
Subject * sub3 = [[Subject alloc] init];
[sub1 setSubject:@"C" hours:3.0 score:98];
[sub2 setSubject:@"C++" hours:3.5 score:90];
[sub3 setSubject:@"Objective-C" hours:3.5 score:92];
[SubArray addObject:sub1];
[SubArray addObject:sub2];
[SubArray addObject:sub3];
// 编码写入文件
if (YES == [NSKeyedArchiver archiveRootObject:SubArray toFile:path])
{
NSLog(@"文件写入成功...");
}
}
return 0;
}
程序首先建立三个 Subject 类型对象,并填入科目名称、上课时数与分数后,将它们依次存入 SubArray 数组。此时以 NSKeyedArchiver 的类别方法 archiveRootObject:toFile: 编码为 plist 格式后,存入 path 指定的文件。在编码 SubArray 的过程中,它的三个元素会先后进行编码,此是各类别的 encodeWithCoder: 方法依序被调用,依照方法设计的逻辑编码。下面的范例 archive_class_read 能将文件 subject.plist 复原为数组。
范例程序 archive_class_read
// archive_class_read
#import
#import "Subject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 文件储存路径
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"subject.plist"];
NSMutableArray *SubArray;
SubArray = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
for (Subject *temp in SubArray) {
[temp print];
}
}
return 0;
}
输出结果
Subject:C, Hours:3, Score:98
Subject:C++, Hours:3.5, Score:90
Subject:Objective-C, Hours:3.5, Score:92
Program ended with exit code: 0
程序的重点是译码的过程中,触发各元素内的 initWithCoder: 方法,将文件的内容还原至 SubArray。
四、使用 NSData 定制 Archive 的程序
上述使用 NSKeyedArchiver(NSKeyedUnarchiver) 类别方法 archiveRootObject:toFile:(unarchiveObjectWithFile:),都是文件直接写入方式。若搭配文件输入与输出谈到的技巧,将多个需要的内容编码汇集后,再写入文件。这么做的好处能让程序更具弹性,并且在 I/O 的存取上更为快速。
范例程序 archive_nsdata_write
// archive_nsdata_write
#import
#import "Subject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 储存路径
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"data.plist"];
// 创建Subject对象
Subject *sub1 = [[Subject alloc] init];
Subject *sub2 = [[Subject alloc] init];
[sub1 setSubject:@"Java" hours:4 score:80];
[sub2 setSubject:@"Python" hours:3 score:82];
// 创建NSData并与Archiver关联
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
initForWritingWithMutableData:data];
[archiver encodeObject:sub1 forKey:@"SUBJECT1_KEY"];
[archiver encodeObject:sub2 forKey:@"SUBJECT2_KEY"];
[archiver finishEncoding];
// 写入文件中
if (YES == [data writeToFile:path atomically:YES]) {
NSLog(@"文件写入成功...");
} else {
NSLog(@"文件写入失败...");
}
}
return 0;
}
首先建立并初始化两个 Subject 对象,接着建立 NSKeyedArchiver 类别对象 archiver,并且设定存入的数据暂存区域为 data,这里可以想象为一个串流。然后 archiver 使用 encodeObject:forKey: 方法,将 sub1 与 sub2 以对应的键值 SUBJECT1_KEY、SUBJECT1_KEY进行编码。请注意,这二行语句结束后,编码动作还没有执行,它仅是记载该被编码的对象是谁,直到下一行语句 [archiver finishEncoding]; 结束后,才会将对象编码并记录于 data 对象中,最后再利用 NSMutableData 的父类别方法 writeToFile:atomically: 将 data 对象写入文件。请注意,NSKeyedArchiver 默认是以二进制的 plist 储存,好处是文件较小,若想改以 XML plist 的格式储存,则可以加入下面这行语句:
[archiver setOutputFormat:NSPropertyListXMLFormat_v1_0];
请注意,NSKeyedUnarchiver 不需要设定输入格式,它会自动判断。这里除了这两种格式外,Mac OS X 另外支持自 NextStep 而来的 NSPropertyListOpenStepFormat,也称之为旧格式 ASCII plist,但因为是旧格式,因此只可读取不可写入。plist 可用的三种格式如下表所示:
plist 格式 | 说明 |
---|---|
NSPropertyListOpenStepFormat | old-style ASCII plist,旧格式,只可进行读取 |
NSPropertyListXMLFormat | XML plist,能以纯文本编辑器直接编辑 |
NSPropertyListBinaryFormat | Binary plist,同样内容的情况下,比较XML plist文件还小 |
范例程序 archive_nsdata_read 能将上一个范例输出的文件 data.plist 解码还原。
范例程序 archive_nsdata_read
// archive_nsdata_read
#import
#import "Subject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 储存路径
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"data.plist"];
// 创建Subject对象
Subject *sub1;
Subject *sub2;
// 将储存路径 path 传给 data 读取,并设定 unarchiver 初始参数
NSMutableData *data = [NSMutableData dataWithContentsOfFile:path];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
initForReadingWithData:data];
// 以对应的键值进行译码
sub1 = [unarchiver decodeObjectForKey:@"SUBJECT1_KEY"];
sub2 = [unarchiver decodeObjectForKey:@"SUBJECT2_KEY"];
// 关闭 unarchiver,结束解码动作
[unarchiver finishDecoding];
// 显示 sub1 与 sub2 的内容
[sub1 print];
[sub2 print];
}
return 0;
}
较为重要的地方在于,译码与编码过程完全是相反的,首先利用 NSMutableData 的打开文件方法,将 data.plist 文件读入 data 串流,接着建立并初始化 NSKeyedUnarchiver,这里是将 data 串流设定为 unarchiver 欲解码的串流。前置作业结束后,主要的解码便是将指定的键值所对应的对象,返回给欲参考的变量 sub1 与 sub2,最后别忘记了将 finishDecoding 信息传送给 unarchiver,以完成译码的相关动作。
五、使用 Archiver 完成深层复制
由于 archive 保留对象图形的结构与内容,因此能利用这个特性达到深层复制。下面的范例程序 archvie_nsdata_copy 示范如何进行此项功能。
范例程序 archvie_nsdata_copy
// archvie_nsdata_copy
#import
#import "Subject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSData *data;
NSMutableArray *array1 = [NSMutableArray arrayWithObjects:
[NSMutableString stringWithString:@"no.1"],
[NSMutableString stringWithString:@"no.2"],
[NSMutableString stringWithString:@"no.3"],
nil];
NSMutableArray *array2;
NSMutableString *str;
// 显示出数组参考的地址
NSLog(@"array1对象的地址:%lx",array1);
NSLog(@"array1的元素地址:");
for (int index = 0; index < [array1 count]; index ++) {
NSLog(@"index %i: %lx", index, [array1 objectAtIndex:index]);
}
// 显示出两数组修改前的内容
NSLog(@"======修改前======");
NSLog(@"array1的元素:");
for (NSString *temp in array1) {
NSLog(@"%@",temp);
}
NSLog(@"array2的元素:");
for (NSString *temp in array2) {
NSLog(@"%@",temp);
}
// 以 data 当作中介,将 array1 放进 data 后,再给 array2
data = [NSKeyedArchiver archivedDataWithRootObject:array1];
array2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
// 经由 NSKeyedArchiverr 的运行后...
NSLog(@"经由 NSKeyedArchiverr 的运行后...");
NSLog(@"array2对象的地址:%lx",array2);
NSLog(@"array2的元素地址:");
for (int index = 0; index < [array2 count]; index ++) {
NSLog(@"index %i: %lx", index, [array2 objectAtIndex:index]);
}
// 修改 array2 位于 index 为 0 的位置,在最后插入字符串
str = [array2 objectAtIndex:0];
[str appendString:@"additional"];
// 显示出两数组的内容,以验证复制结果
NSLog(@"======修改后======");
NSLog(@"array1的元素:");
for (NSString *temp in array1) {
NSLog(@"%@",temp);
}
NSLog(@"array2的元素:");
for (NSString *temp in array2) {
NSLog(@"%@",temp);
}
}
return 0;
}
程序首先建立具有三个 NSMutableString 元素的 NSMutableArray,接着以语句:
data = [NSKeyedArchiver archivedDataWithRootObject:array1];
将根对象 array1 进行编码,返回的内容为 NSMutableData 串流对象并指定给 data,接着语句:
array2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
将串流对象 data 当作参数,让 NSKeyedUnarchiver 将此串流的内容进行译码,因此 array2 得到一个 id 类型的对象,由此可知,此方法只适合 objective-C 的对象,因此标准 C 语言的对象,得先转换成 NSNumber 或 NSString 等对象,才适用此方法。为了验证此方法,这里先将原数组内容显示出来后,再改变 array2 位于索引值为 0 的对象内容,接着再显示出这两个数组,输出结果如下:
输出结果
array1对象的地址:1002006d0
array1的元素地址:
index 0: 100200400
index 1: 100200490
index 2: 1002004d0
======修改前======
array1的元素:
no.1
no.2
no.3
array2的元素:
经由 NSKeyedArchiverr 的运行后...
array2对象的地址:100603150
array2的元素地址:
index 0: 100602080
index 1: 100602370
index 2: 1006023b0
======修改后======
array1的元素:
no.1
no.2
no.3
array2的元素:
no.1additional
no.2
no.3
此方法可以解决相当多隐藏的复制问题,若欲建立的应用程序,无法以 NSCoding 协议所提出的解决方案处理,那么使用此方法将这将它实现 Copy 或 MutableCopy 方法中,以达深层复制的功能。
小结
本章介绍了 Foundation 框架的归档和序列化类。使用 Foundation 框架归档和序列化类可以将对象(对象图)转换为具有独立结构的字节缓冲区。这样就可以将数据写入文件或者传送给其他进程(通常会通过网络)。之后,这些数据可能会被转换回对象并保留相关的对象图。序列化处理类能够保存数据和对象在其层次结构中的位置,而归档处理类具有更广泛的用途,它们可以保存数据、数据类型和对象层次结构中对象之间的关系。