Cocoa编程中的文件处理:属性列表与对象编码详解
在Cocoa编程中,文件的加载和保存是一项常见且重要的任务。许多计算机程序都会生成用户工作的半永久性成果,如编辑后的照片、小说章节或音乐翻唱等,这些最终都会以文件形式保存下来。下面将详细介绍Cocoa中文件处理的两种主要方式:属性列表和对象编码。
1. 属性列表(Property Lists)
属性列表对象(Property List Objects),常缩写为plist,是Cocoa中一类特殊的对象。这些列表包含了一组Cocoa能够处理的对象,特别是可以将它们保存到文件并重新加载。属性列表类包括
NSArray
、
NSDictionary
、
NSString
、
NSNumber
、
NSDate
和
NSData
,以及它们的可变版本(如果有的话)。
1.1 NSDate
在程序中,时间和日期的处理非常常见。例如,iPhoto知道你拍摄狗狗照片的日期,个人会计应用程序知道银行对账单的结算日期。
NSDate
是Cocoa中处理日期和时间的基础类。
- 获取当前日期和时间:
NSDate *date = [NSDate date];
NSLog (@"today is %@", date);
输出示例:
today is 2012-01-23 11:32:02 -0400
- 获取指定时间间隔的日期:
NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow: -(24 * 60 * 60)];
NSLog (@"yesterday is %@", yesterday);
+dateWithTimeIntervalSinceNow:
方法接受一个
NSTimeInterval
类型的参数,它是一个表示秒数间隔的双精度浮点数。通过正的时间间隔可以指定未来的时间,负的时间间隔则指定过去的时间。
如果需要格式化日期的输出,Apple提供了
NSDateFormatter
类,它符合Unicode技术标准#35,可以为用户提供各种日期格式。
1.2 NSData
在C语言中,常见的做法是将数据缓冲区传递给函数。通常需要传递缓冲区的指针和长度,并且还会涉及内存管理问题。例如,如果缓冲区是动态分配的,当它不再使用时,谁负责清理它呢?
Cocoa提供了
NSData
类来封装字节块。可以获取数据的长度和字节的起始指针。由于
NSData
是一个对象,遵循常规的内存管理规则。如果要将数据块传递给函数或方法,可以传递一个自动释放的
NSData
对象,而不必担心清理问题。
const char *string = "Hi there, this is a C string!";
NSData *data = [NSData dataWithBytes: string length: strlen(string) + 1];
NSLog (@"data is %@", data);
输出结果:
data is <48692074 68657265 2c207468 69732069 73206120 43207374 72696e67 2100>
通过ASCII码表可以看出,这一串十六进制实际上就是我们的字符串。
-length
方法返回字节数,
-bytes
方法返回字符串的起始指针。注意
+dataWithBytes:
调用中的
+ 1
,这是为了包含C字符串所需的尾随零字节。通过包含零字节,可以使用
%s
格式说明符来打印字符串:
NSLog (@"%d byte string is '%s'", [data length], [data bytes]);
输出结果:
30 byte string is 'Hi there, this is a C string!'
NSData
对象是不可变的,一旦创建就不能更改。而
NSMutableData
允许添加和删除数据内容中的字节。
1.3 读写属性列表
集合属性列表类(
NSArray
和
NSDictionary
)有一个
-writeToFile:atomically:
方法,用于将属性列表写入文件。
NSString
和
NSData
也有这个方法,但它只是写入字符串或数据块。
- 保存属性列表:
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
中的系统配置文件。
- 读取属性列表:
NSArray *phrase2 = [NSArray arrayWithContentsOfFile: @"/tmp/verbiage.txt"];
NSLog (@"%@", phrase2);
输出结果:
(
I,
seem,
to,
be,
a,
verb
)
需要注意的是,一些属性列表文件,特别是偏好设置文件,以压缩二进制格式存储。可以使用
plutil
命令将这些文件转换为人类可读的格式:
plutil -convert xml1 filename.plist
writeToFile:atomically:
方法中的
atomically:
参数是一个布尔值,它告诉Cocoa是否应该先将文件内容保存到临时文件中,当文件保存成功后,再将临时文件与原始文件交换。这是一种安全机制,如果保存过程中出现问题,不会覆盖原始文件。但这种安全机制也有代价,在保存过程中会占用双倍的磁盘空间,因为原始文件仍然存在。除非要保存的文件非常大,可能会填满用户的硬盘,否则建议以原子方式保存文件。
这些函数的一个缺点是它们不返回任何错误信息。如果无法加载文件,方法只会返回一个空指针,而不知道具体出了什么问题。
1.4 修改对象
当使用集合类型从文件中读取数据时,不能直接修改数据。一种修改方法是使用暴力方式,遍历plist并创建一个可变的并行结构。但还有另一种更简单的方法。
NSPropertyListSerialization
类可以按照你想要的选项保存和加载属性列表。
- 以二进制格式写入plist数据:
NSString *error = nil;
NSData *encodedArray = [NSPropertyListSerialization dataFromPropertyList:capitols
format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
[encodedArray writeToFile:@"/tmp/capitols.txt" atomically:YES];
- 从文件中读取数据:
NSPropertyListFormat propertyListFormat = NSPropertyListXMLFormat_v1_0;
NSString *error = nil;
NSMutableArray *capitols = [NSPropertyListSerialization propertyListFromData:data
mutabilityOption:NSPropertyListMutableContainersAndLeaves
format:&propertyListFormat
errorDescription:&error];
在读取数据时,可以选择是否能够修改plist,以及是修改列表结构还是仅修改数据。
2. 编码对象
并非所有对象的信息都能表示为属性列表类。如果所有内容都可以表示为数组字典,那么就不需要自己的类了。幸运的是,Cocoa提供了一种机制,让对象可以将自己转换为可以保存到磁盘的格式。对象可以将其实例变量和其他数据编码为一个数据块,保存到磁盘。这个数据块可以在以后读回到内存中,并根据保存的数据创建新的对象。这个过程称为编码和解码,或序列化和反序列化。
要让自己的对象实现这个功能,可以采用
NSCoding
协议。协议定义如下:
@protocol NSCoding
- (void) encodeWithCoder: (NSCoder *) encoder;
- (id) initWithCoder: (NSCoder *) decoder;
@end
通过采用这个协议,需要实现这两个方法。当对象被要求保存自己时,会调用
-encodeWithCoder:
方法;当对象被要求加载自己时,会调用
-initWithCoder:
方法。
NSCoder
是一个抽象类,定义了一组将对象转换为
NSData
并转换回来的有用方法。通常不会创建新的
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
@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.magicNumber = mn;
self.shoeSize = ss;
self.subThingies = [NSMutableArray array];
}
return (self);
}
- (void) dealloc
{
[name release];
[subThingies release];
[super dealloc];
}
- (void) encodeWithCoder: (NSCoder *) coder
{
[coder encodeObject: name forKey: @"name"];
[coder encodeInt: magicNumber forKey: @"magicNumber"];
[coder encodeFloat: shoeSize forKey: @"shoeSize"];
[coder encodeObject: subThingies forKey: @"subThingies"];
}
- (id) initWithCoder: (NSCoder *) decoder
{
if (self = [super init]) {
self.name = [decoder decodeObjectForKey: @"name"];
self.magicNumber = [decoder decodeIntForKey: @"magicNumber"];
self.shoeSize = [decoder decodeFloatForKey: @"shoeSize"];
self.subThingies = [decoder decodeObjectForKey: @"subThingies"];
}
return (self);
}
- (NSString *) description
{
NSString *description =
[NSString stringWithFormat: @"%@: %d/%.1f %@", name,
magicNumber, shoeSize, subThingies];
return (description);
}
@end // Thingie
在
main()
函数中使用这个类:
Thingie *thing1;
thing1 = [[Thingie alloc] initWithName: @"thing1" magicNumber: 42 shoeSize: 10.5];
NSLog (@"some thing: %@", thing1);
NSData *freezeDried;
freezeDried = [NSKeyedArchiver archivedDataWithRootObject: thing1];
[thing1 release];
thing1 = [NSKeyedUnarchiver unarchiveObjectWithData: freezeDried];
NSLog (@"reconstituted thing: %@", thing1);
输出结果:
some thing: thing1: 42/10.5 ( )
reconstituted thing: thing1: 42/10.5 ( )
可以向
subThingies
数组中添加对象,当数组被编码时,这些对象会自动被编码。
Thingie *anotherThing;
anotherThing = [[[Thingie alloc]
initWithName: @"thing2"
magicNumber: 23
shoeSize: 13.0] autorelease];
[thing1.subThingies addObject: anotherThing];
anotherThing = [[[Thingie alloc]
initWithName: @"thing3"
magicNumber: 17
shoeSize: 9.0] autorelease];
[thing1.subThingies addObject: anotherThing];
NSLog (@"thing with things: %@", thing1);
输出结果:
thing with things: thing1: 42/10.5 (
thing2: 23/13.0 (
),
thing3: 17/9.0 (
)
)
总结
通过属性列表和对象编码,Cocoa提供了强大而灵活的文件处理功能。属性列表适用于将数据简化为特定类型的情况,可以方便地保存和加载数据。对象编码则允许将自定义对象保存到磁盘并恢复,为程序的持久化存储提供了更多的可能性。在实际开发中,可以根据具体需求选择合适的文件处理方式。
下面是一个简单的流程图,展示了对象编码和解码的过程:
graph LR
A[创建对象] --> B[编码对象]
B --> C[保存为NSData]
C --> D[写入文件]
D --> E[从文件读取]
E --> F[转换为NSData]
F --> G[解码对象]
G --> H[恢复对象]
表格总结属性列表类及其用途:
| 类名 | 用途 |
| ---- | ---- |
| NSArray | 存储有序的对象集合 |
| NSDictionary | 存储键值对的集合 |
| NSString | 存储字符串 |
| NSNumber | 存储数字 |
| NSDate | 存储日期和时间 |
| NSData | 存储二进制数据 |
3. 深入理解对象编码与属性列表的应用场景
3.1 对象编码的优势与适用场景
对象编码在处理自定义对象时具有显著优势。当程序中存在复杂的对象结构,且需要将这些对象持久化存储到磁盘时,对象编码就派上了用场。例如,在一个游戏开发中,游戏角色可能包含多个属性,如生命值、魔法值、装备列表等,这些属性可以封装在一个自定义的
GameCharacter
类中。通过采用
NSCoding
协议,可以将游戏角色的状态保存到文件中,在下次启动游戏时恢复角色的状态。
以下是一个简单的
GameCharacter
类示例:
@interface GameCharacter : NSObject <NSCoding>
{
NSString *name;
int health;
int mana;
NSMutableArray *equipments;
}
@property (copy) NSString *name;
@property int health;
@property int mana;
@property (retain) NSMutableArray *equipments;
- (id)initWithName: (NSString *) n health: (int) h mana: (int) m;
@end
@implementation GameCharacter
@synthesize name;
@synthesize health;
@synthesize mana;
@synthesize equipments;
- (id)initWithName: (NSString *) n health: (int) h mana: (int) m
{
if (self = [super init]) {
self.name = n;
self.health = h;
self.mana = m;
self.equipments = [NSMutableArray array];
}
return (self);
}
- (void) dealloc
{
[name release];
[equipments release];
[super dealloc];
}
- (void) encodeWithCoder: (NSCoder *) coder
{
[coder encodeObject: name forKey: @"name"];
[coder encodeInt: health forKey: @"health"];
[coder encodeInt: mana forKey: @"mana"];
[coder encodeObject: equipments forKey: @"equipments"];
}
- (id) initWithCoder: (NSCoder *) decoder
{
if (self = [super init]) {
self.name = [decoder decodeObjectForKey: @"name"];
self.health = [decoder decodeIntForKey: @"health"];
self.mana = [decoder decodeIntForKey: @"mana"];
self.equipments = [decoder decodeObjectForKey: @"equipments"];
}
return (self);
}
- (NSString *) description
{
NSString *description =
[NSString stringWithFormat: @"%@: Health %d, Mana %d, Equipments %@", name,
health, mana, equipments];
return (description);
}
@end
在
main()
函数中使用这个类:
GameCharacter *character;
character = [[GameCharacter alloc] initWithName: @"Warrior" health: 100 mana: 50];
NSLog (@"Character: %@", character);
NSData *characterData;
characterData = [NSKeyedArchiver archivedDataWithRootObject: character];
[character release];
character = [NSKeyedUnarchiver unarchiveObjectWithData: characterData];
NSLog (@"Restored Character: %@", character);
3.2 属性列表的局限性与应对策略
属性列表虽然方便,但也有一定的局限性。正如前面提到的,它只能处理特定类型的对象,并且不返回错误信息。当数据结构较为复杂,无法简单地用属性列表类表示时,属性列表就不太适用了。
为了应对这些局限性,可以结合使用属性列表和对象编码。例如,在一个应用程序中,需要保存用户的设置和一些自定义对象。可以将用户设置保存为属性列表,而将自定义对象使用对象编码保存。
以下是一个简单的示例,展示如何结合使用属性列表和对象编码:
// 自定义对象
@interface CustomObject : NSObject <NSCoding>
{
NSString *info;
}
@property (copy) NSString *info;
- (id)initWithInfo: (NSString *) i;
@end
@implementation CustomObject
@synthesize info;
- (id)initWithInfo: (NSString *) i
{
if (self = [super init]) {
self.info = i;
}
return (self);
}
- (void) dealloc
{
[info release];
[super dealloc];
}
- (void) encodeWithCoder: (NSCoder *) coder
{
[coder encodeObject: info forKey: @"info"];
}
- (id) initWithCoder: (NSCoder *) decoder
{
if (self = [super init]) {
self.info = [decoder decodeObjectForKey: @"info"];
}
return (self);
}
- (NSString *) description
{
return [NSString stringWithFormat: @"Custom Object: %@", info];
}
@end
// 主函数
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 自定义对象
CustomObject *custom = [[CustomObject alloc] initWithInfo: @"Some info"];
NSData *customData = [NSKeyedArchiver archivedDataWithRootObject: custom];
// 用户设置
NSDictionary *settings = @{@"theme": @"dark", @"fontSize": @16};
[settings writeToFile: @"/tmp/settings.plist" atomically: YES];
// 保存自定义对象数据
[customData writeToFile: @"/tmp/custom.data" atomically: YES];
// 读取用户设置
NSDictionary *readSettings = [NSDictionary dictionaryWithContentsOfFile: @"/tmp/settings.plist"];
NSLog (@"Settings: %@", readSettings);
// 读取自定义对象数据
NSData *readData = [NSData dataWithContentsOfFile: @"/tmp/custom.data"];
CustomObject *restoredCustom = [NSKeyedUnarchiver unarchiveObjectWithData: readData];
NSLog (@"Restored Custom Object: %@", restoredCustom);
[custom release];
}
return 0;
}
3.3 性能考虑
在使用属性列表和对象编码时,性能也是一个需要考虑的因素。属性列表的读写操作相对简单,性能较高,但对于复杂的数据结构,可能需要花费更多的时间来组织数据。对象编码在处理复杂对象时更加灵活,但编码和解码过程可能会消耗更多的时间和内存。
以下是一个简单的性能测试示例,比较属性列表和对象编码的读写性能:
// 性能测试
NSArray *largeArray = [NSArray arrayWithObjects: @"item1", @"item2", @"item3", nil];
// 属性列表写入测试
NSDate *startTime = [NSDate date];
[largeArray writeToFile: @"/tmp/largeArray.plist" atomically: YES];
NSDate *endTime = [NSDate date];
NSTimeInterval plistWriteTime = [endTime timeIntervalSinceDate: startTime];
NSLog (@"Property List Write Time: %f seconds", plistWriteTime);
// 属性列表读取测试
startTime = [NSDate date];
NSArray *readArray = [NSArray arrayWithContentsOfFile: @"/tmp/largeArray.plist"];
endTime = [NSDate date];
NSTimeInterval plistReadTime = [endTime timeIntervalSinceDate: startTime];
NSLog (@"Property List Read Time: %f seconds", plistReadTime);
// 对象编码写入测试
startTime = [NSDate date];
NSData *encodedArray = [NSKeyedArchiver archivedDataWithRootObject: largeArray];
[encodedArray writeToFile: @"/tmp/largeArray.data" atomically: YES];
endTime = [NSDate date];
NSTimeInterval encodingWriteTime = [endTime timeIntervalSinceDate: startTime];
NSLog (@"Object Encoding Write Time: %f seconds", encodingWriteTime);
// 对象编码读取测试
startTime = [NSDate date];
NSData *readEncodedData = [NSData dataWithContentsOfFile: @"/tmp/largeArray.data"];
NSArray *restoredArray = [NSKeyedUnarchiver unarchiveObjectWithData: readEncodedData];
endTime = [NSDate date];
NSTimeInterval encodingReadTime = [endTime timeIntervalSinceDate: startTime];
NSLog (@"Object Encoding Read Time: %f seconds", encodingReadTime);
4. 总结与最佳实践
4.1 总结
属性列表和对象编码是Cocoa中处理文件加载和保存的两种重要方式。属性列表适用于简单的数据结构,能够方便地保存和加载特定类型的对象。对象编码则适用于处理复杂的自定义对象,通过采用
NSCoding
协议,可以将对象的状态保存到文件中并恢复。
4.2 最佳实践
- 选择合适的方法 :根据数据结构的复杂程度选择合适的文件处理方法。如果数据可以用属性列表类表示,优先使用属性列表;如果数据结构复杂,使用对象编码。
- 错误处理 :由于属性列表和对象编码函数不返回错误信息,在实际开发中需要添加额外的错误处理机制。例如,可以在保存和加载文件时,检查文件是否存在、是否有足够的磁盘空间等。
- 性能优化 :在处理大量数据时,需要考虑性能问题。可以通过优化数据结构、减少不必要的编码和解码操作等方式来提高性能。
以下是一个简单的流程图,展示了如何选择合适的文件处理方法:
graph LR
A[数据结构] --> B{简单?}
B -- 是 --> C[属性列表]
B -- 否 --> D[对象编码]
表格总结属性列表和对象编码的优缺点:
| 方法 | 优点 | 缺点 |
| ---- | ---- | ---- |
| 属性列表 | 方便、简单,适用于简单数据结构 | 只能处理特定类型的对象,不返回错误信息 |
| 对象编码 | 灵活,适用于复杂的自定义对象 | 编码和解码过程可能消耗更多时间和内存 |
通过合理使用属性列表和对象编码,可以在Cocoa编程中高效地处理文件的加载和保存,为程序的持久化存储提供强大的支持。
超级会员免费看
65

被折叠的 条评论
为什么被折叠?



