29、Cocoa编程中的文件处理:属性列表与对象编码详解

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编程中高效地处理文件的加载和保存,为程序的持久化存储提供强大的支持。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值