第十五章 文件加载与保存
标准的C函数库提供了函数调用来创建、读取和写入文件,例如open()、read()、write()、fopen()、fread()等。Cocoa提供了Core Data,他能在后台处理所有文件内容,这些都不再讨论。
那么,我们还需要做什么呢?Cocoa提供了两个通用的文件处理类:属性列表和对象编码。
15.1 属性列表
在Cocoa中,有一类名为属性列表的对象,常简写为plist。这些列表包含Cocoa知道如何操作的一组对象。具体来讲,Cocoa知道如何将他们保存到文件中进行加载。属性列表类包括NSArray、NSDictionary、NSString、NSNumber、NSDate和NSData,以及他们的变体。
前面的章节已经介绍了前4种对象:NSArray、NSDictionary、NSString、NSNumber。下面介绍后两种:NSDate和NSData。
15.1.1 NSDate
可以使用[NSDate date]获取当前的日期和时间,他是一个自动释放对象。例子:
NSDate *date=[NSDate date];
NSLog(@"today is %@",date);
输出 today is 2008-08-23 11:32:02 -400
你可以对日期进行比较而进行排序,还可以获取与当前时间相隔一定时差的日期,例如:
NSDate *yesterday=[NSDate dateWithTimeIntervalSinceNow:-(24*60*60)];
+dateWithTimeIntervalSinceNow:接受一个NSTimeInterval参数,该参数是一个双精度值,表示以秒为单位的时间间隔。
15.1.2 NSData
将数据缓冲区传递给函数是C中的常见操作。为此,你通常将缓冲区的指针和长度传递给某个函数。另外,C语言中可能会出现内存管理问题。例如,如果缓冲区已经被动态分配,那么当他不再使用时,由谁负责将其清除?
Cocoa为我们提供了NSData类,该类包装了大量字节。你可以获得数据的长度和指向字节起始位置的指针。因为NSData是一个对象,适用于常规的内存管理行为。因此,如果将数据块传递给一个函数或方法,可以通过传递一个自动释放NSData来实现,无需担心内存清除问题。下面的NSData对象将保存一个普通的C字符串,然后输出数据:
const char *string="Hi there,this is a C string";
NSData *data=[NSData dateWithBytes:string length:strlen(string)+1];
NSLog(@"data is %@",data);
输出:data is <48592074 ... ... ...2100>
上面输出的是字符的ASCII码。
-length方法给出字节数
-bytes方法给出指向字符串起始位置的指针
注意到+dataWithBytes:中的+1了吗?他用于包含C字符串所需的尾部的零字节。你还会注意到NSLog结果末尾的00,通过包含零字节,可以使用%s格式的说明符来输出字符串
NSLog(@"%d byte string is '%s'",[data length],[data bytes]);
NSData对象是不可改变的,他们被创建后就不能改变。可以使用他们,但不能更改其中的内容。但是NSMutableData支持在数据内容中添加和删除字节
15.1.3 写入和读取属性列表
你已经看到了所有属性列表类,怎么使用他们呢?集合属性列表类(NSArray、NSDictionary)具有一个-writeToFile:atomically:方法,用于将属性列表写入文件。NSString和NSData也具有writeToFile:atomically:方法,但他们只能写出字符串或数据块。
因此,我们可以将字符串存入一个数组,然后保存该数组:
NSArray *phrase;
phrase=[NSArray arrayWithObjects:@"I",@"seem",@"to",@"be",@"a",@"verb",nil};
[phrase writeToFile:@"/tmp/verbiage.txt",atomically:YES];
现在看一下文件/tmp/verbiage.txt,你应该看到如下代码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<string>I</string>
<string>seem</string>
<string>to</string>
<string>be</string>
<string>a</string>
<string>verb</string>
</array>
</plist>
Xcode还包含一个属性列表编辑器,所以可以查看plist文件并进行编辑。如果看一下操作系统,你会发现许多属性列表文件和系统配置文件,如主目录Library/Preferences下的所有首选项文件和/System/Library/LaunchDaemons下的系统配置文件。
说明:有些属性列表文件(特别是首选项文件)是以压缩的二进制格式存储的。通过使用plutil命令:
plutil -convert xml1 filename.plist,可以将这些文件转换成人们可读的形式。
现在已经将verbiage.txt存放在磁盘上,可以使用+arrayWithContentsOfFile:方法读取该文件:
NSArray *phrase2=[NSArray arrayWithContentsOfFile:@"/tmp/verbiage.txt"];
NSLog(@"%@",phrase2);
说明:注意到writeToFile:方法中的单词atomically了吗?这种调用有什么负面作用吗?没有。atomically:参数的值为BOOL类型,用于通知Cocoa是否应该首先将文件内容保存在临时文件中,当文件成功保存后,在将该临时文件和原始文件交换。这是一种安全机制:如果在保存过程中出现意外,不会破坏原始文件,但这种安全机制需要付出一定的代价:在保存过程中,由于原始文件仍然保存在磁盘中,所以需要使用双倍的磁盘空间。除非保存的文件非常大,将会占用用户硬盘的空间,否则应该自动保存文件。
如果能将数据精简为属性列表类型,则可以使用这些非常便捷的调用来将内容保存在磁盘中,供以后读取。如果你正在从事一项新创意或设计一个新项目,可以使用这些便捷方法来快速编写和运行程序。即使只想把数据块保存在磁盘中,并且根本不需要使用对象,也可以使用NSData来简化工作。只需要将数据包装在一个NSData对象中,然后在NSData对象上调用writeToFile:atomically:。
这些函数的一个缺点是,他们不会返回任何错误信息。如果不能加载文件,只能从方法中得到nil指针,而不能确定出现了何种错误。
15.2 编码对象
遗憾的是,无法总是将对象信息表示为属性列表类。如果能将所有对象都表示为数组字典,那我们就没有必要使用自己的类了。所幸,Cocoa具备一种机制来将对象自身转换为某种格式并保存在磁盘中。对象可以将他们的实例变量和其他数据编码为数据块,然后保存在磁盘中,以后将这些数据库读回内存中,并且还能基于保存的数据创建新对象。这个过程称为 编码和解码,或 序列化和反序列化。
在底14章中介绍Interface Builder时,我们从库中将对象拖到窗口中,这些对象被保存到nib文件中。换言之,NSWindow和NSTextField对象都被序列化并保存在磁盘中。当程序运行时,会将nib文件加载到内存中,串行化对象,同时创建新的NSWindow和NSTextField对象并将其相互关联起来。
你可能会猜到,通过采用NSCoding协议,可以使用自己的对象实现相同的功能。该协议与下面的代码类似:
@protocol NSCoding
- (void) encodeWithCode:(NSCoder *)aCoder;
- (id) initWithCoder:(NSCoder *)aDecoder;
@end
那么这个编码器是什么呢?NSCoder是一个抽象类,定义一些有用的方法来在对象与NSData之间来回转换,完全不需要创建新NSCoder,因为他实际上并无多大作用。但是我们实际上要使用NSCoder的一些具体的子类来编码和解码对象。我们使用其中两个子类NSKeyedArchiver和NSKeyedUnarchiver。
还是看下例子吧。
首先看一下一个含有实例变量的简单类:
@interface Thingie:NSObject <NSCoding> {
NSString *name;
int magicNumber;
float shoeSize;
NSMutableArray *subThingies;
}
@property (copy) NSString *name;
@property int magicNumber;
@property float shoeSize;
@property (retain) NSMutableArray *subThingies;
- (id) initWithName:(NSString *)n magicNumber:(int)mn shoeSize:(float)ss;
@end //Thingie
注意,Thingie类采用NSCoding协议,这意味着我们将实现encodeWithCoder:和initWithCoder:方法,至于现在,我们先将这两个方法设置为空。
@implementation Thingie
@synthesize name;
@synthesize magicNumber;
@synthesize shoeSize;
@synthesize subThingies;
- (id) initWithName:(NSString *)n magicNumber:(int)mn shoeSize:(float)ss {
if (self=[super init]){
self.name=n;
self.magincNumber=mn;
self.shoeSize=ss;
self.subThingies=[NSMutableArray array];
}
return (self);
}
- (void) dealloc {
[name release];
[subThingies release];
[super dealloc];
}//dealloc
- (void) encodeWithCoder:(NSCoder *)coder {
//nobody home
}//encodeWithCoder
- (id) initWithCoder:(NSCoder *)decoder {
return (nil);
}//initWithCoder
- (NSString *)description {
NSString *desc=[NSString stringWithFormat:@"%@:%d/%.lf %@",name,magicNumber,shoeSize,subThinges];
return desc;
}//description
@end //Thingie
以上代码将初始化一个新对象,清楚我们生成的所有无用信息,创建存根方法来使编译器适合NSCoding协议,并返回相关描述
注意,在init方法中,我们在赋值表达式的左边使用了self.attribute。请记住,这实际上意味着我们需要为这些属性调用访问方法,这些方法由@synthesize创建,我们不直接对实例变量赋值。这种对象创建技术将为传入的NSString和我们创建的NSMutableArray提供适当的内存管理方法,因此我们不必为这些对象明确提供内存管理方法。
因此,在main()函数中,我们构造一个Thingie并输出:
Thingie *thing1;
thing1=[[Thingie alloc] initWithName:@"thing1" magicNumber:42 shoeSize:10.5];
NSLog(@"some thing %@",thing1);
现在我们保存该对象。并按以下方式实现Thingie的encodeWithCoder:
- (void) encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:name forKey:@"name"];
[coder encodeInt:magicNumber forKey:@"magicNumber"];
[coder encodeFloat:shoeSize forKey:@"shoeSize"];
[coder encodeObject:subThingies forKey:@"subThingies"];
}//encodeWithCoder
我们将使用NSKeyedArchiver把对象归档到NSData中,顾名思义,KeyedArchiver使用键/值对保存对象的信息。Thingie的-encodeWithCoder方法在与每个实例变量名称匹配的键下编码这些实例变量,也可以不这样做,可以在键flarblewhazzit下编码实例变量名称。使键的名称与实例变量的名称尽可能相似,这样便于识别他们之间的映射关系。
可以使用这样的裸字符串做为编码键,也可以定义一个常量来避免录入错误。可以使用
#define kSubthingiesKey @"subThingies
等定义常量,也可以使用文件的局部变量,例如:
static NSString *kSubthingiesKey=@"subThingies"
注意,每种类型都有不同的encodeSomething:forKey:。一定要使用合适的方法来对类型进行编码。对于任何Obj-C对象类型,都可以使用encodeObject:forKey:。
如果需要恢复某个对象,可以使用decodeSomethingForKey方法:
- (id) initWithCoder:(NSCoder *)decoder {
if (self=[self init]){
self.name=[decoder decodeObjectForKey:@"name"];
self.magicNumber=[decoder decodeIntForKey:@"magicNumber"];
self.shoeSize=[decoder decodeFloatForKey:@"shoeSize"];
self.subThingies=[decoder decodeObjectForKey:@"subThingies"];
}
return (self);
}//initWithCoder
initWithCoder:和任何其他init方法一样,在对对象执行操作之前,需要使用超类对他们进行初始化。为此,可以采用两种方式,具体取决于父类。如果父类采用NSCoding协议,则应该调用[super initWithCoder:decoder];否则,只需要调用[super init]即可。NSObject不采用NSCoding协议,因此我们可以使用简单的init方法。
当使用decodeIntForKey:时,把一个int值从解码器中取出。当使用decodeObjectForKey:方法时,把一个对象从解码器中取出,在嵌入的对象上递归使用initWithCoder:方法。内存管理的工作方式与你预期的一样:从除alloc、copy和new以外的方法获取对象,因此可以假设对象能被自动释放。我们的属性声明用于确保正确的进行内存管理。
接下来,将实际使用这些对象。我们使用前面创建的thing1对象,并将其归档:
NSData *freezeDried;
freezeDried=[NSKeyedArchiver archivedDataWithRootObject:thing1];
+archivedDataWithRootObject:类方法编码thing1对象。首先,他在后台创建一个NSKeyedArchiver实例;然后,他将NSKeyedArchiver实例传递给对象thing1的-encodeWithCoder:方法。当thing1编码自身的属性时,他可能对其他对象也进行编码,例如字符串、数组以及我们可能输入到该数组中的任何内容,整个对象集合完成对键和值的编码后,具有键/值对的归档程序将所有对象扁平化为一个NSData类并将其返回。
如果愿意,可以使用-writeToFile:atomically:方法将这个NSData类保存到磁盘中。因此,我们将先处理thing1对象,然后通过freezeDried表示法重新创建他,并将它输出。
[thing1 release];
thing1=[NSKeyedUnarchiver unarchiveObjectWithData:freezeDried];
NSLog(@"reconstituted thing :%@",thing1);
你可能对subThingies可变数组很好奇。我们可以将对象放入该数组中,当数组被编码时,这些对象将被自动编码。NSArray的encodeWithCoder:实现在所有对象上调用encodeWithCoder方法,最后使所有对象都被编码。
如果被编码的数据中含有循环会怎么样?例如,如果thing1包含在自身的subThingies数组中会怎么样?thing1会对数组进行编码吗?哪个对象对thing1进行编码?哪个对象对数组进行编码?哪个对象再次对thing1进行编码?以此类推?幸运的是,Cocoa在归档和解压程序实现上非常灵活,能够保存并恢复对象周期。
要查看会出现的结果,将thing1放入其自身的subThingies数组中:
[thing1.subThingies addObject:thing1];
但是,不要尝试在thing1中使用NSLog类,NSLog类不能检测对象循环,他将执行一个无限递归来尝试构造日志字符串。
但是,如果现在尝试对thing1进行编码和解码,他将能完美地完成工作。
freezeDried=[NSKeyedArchiver archiveDataWithRootObject:thing1];
thing1=[NSKeyedUnarchiver unarchiveObjectWithData:freezeDried];