Cocoa 开发中的文件处理:属性列表与对象编码
1. 应用套件与文件处理概述
在开发过程中,我们会接触到 Interface Builder 和应用套件。应用套件里有超过 100 种不同的类供我们使用,不过之前我们仅直接使用了一个 AppKit 类(NSTextField),还间接使用了几个类(如驱动按钮的 NSButton 和控制窗口的 NSWindow)。现在,我们可以深入探索 Cocoa 开发了,接下来重点聊聊文件的保存和加载。
许多计算机程序会生成用户工作的半永久性成果,比如编辑后的照片、小说章节或者乐队翻唱的歌曲等,最终都会以保存文件的形式呈现。标准 C 库提供了创建、读取和写入文件的函数,Cocoa 则提供了 Core Data 来处理文件相关操作,但这里我们不讨论这些,而是聚焦于 Cocoa 提供的两种文件处理方式:属性列表和对象编码。
2. 属性列表相关类
属性列表对象(常缩写为 plist)包含一组 Cocoa 能够处理的对象,特别是在文件保存和加载方面。属性列表类有 NSArray、NSDictionary、NSString、NSNumber、NSDate 和 NSData,若有可变版本也包含在内。下面详细介绍 NSDate 和 NSData 这两个之前没详细讲过的类。
2.1 NSDate
在程序里,时间和日期处理很常见。比如 iPhoto 知道你给狗狗拍照的日期,个人记账应用知道银行对账单的结算日期。NSDate 是 Cocoa 中处理日期和时间的基础类。
要获取当前日期和时间,可以使用
[NSDate date]
,它会返回一个自动释放的对象,示例代码如下:
NSDate *date = [NSDate date];
NSLog (@"today is %@", date);
输出结果类似:
today is 2012-01-23 11:32:02 -0400
还有比较两个日期的方法,方便对列表进行排序,也能获取与当前时间有一定间隔的日期。例如,获取 24 小时前的日期:
NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow: -(24 * 60 * 60)];
NSLog (@"yesterday is %@", yesterday);
+dateWithTimeIntervalSinceNow:
方法接受一个
NSTimeInterval
类型的参数,它是一个表示秒数间隔的双精度浮点数。正数表示未来的时间位移,负数表示过去的时间位移。如果想格式化日期的输出,Apple 提供了
NSDateFormatter
类,它符合 Unicode 技术标准 #35。
2.2 NSData
在 C 语言中,常见的做法是将数据缓冲区传递给函数,通常要传递缓冲区的指针和长度,还可能会遇到内存管理问题。Cocoa 提供了 NSData 类来封装字节块,能获取数据的长度和字节起始指针。由于 NSData 是对象,遵循常规的内存管理规则。传递数据块时,可以传递自动释放的 NSData 对象,无需担心清理问题。
下面是一个用 NSData 存储普通 C 字符串并打印数据的示例:
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 允许添加和删除数据内容中的字节。
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
里的系统配置文件。
读取文件可以使用
+arrayWithContentsOfFile:
方法:
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 是否先将文件内容保存到临时文件,保存成功后再用临时文件替换原文件。这是一种安全机制,若保存过程中出现问题,不会覆盖原文件,但会消耗双倍磁盘空间。除非保存的是可能占满用户硬盘的大文件,否则建议以原子方式保存文件。
这些函数的一个缺点是不返回错误信息。若无法加载文件,方法只会返回
nil
指针,不知道哪里出了问题。
4. 对象修改
用集合类型从文件读取数据时,不能直接修改数据。一种修改方法是暴力遍历 plist 并创建可变的并行结构,但有更简单的办法。Cocoa 提供了
NSPropertyListSerialization
类,它能按你想要的选项保存和加载属性列表。
下面是将 plist 数据以二进制格式写入文件的代码:
NSString *error = nil;
NSData *encodedArray = [NSPropertyListSerialization dataFromPropertyList:capitols
format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
[encodedArray writeToFile:@"/tmp/capitols.txt" atomically:YES];
将数组数据转换为 NSData 进行存储。
从文件读取数据到内存时,指定文件格式需要额外步骤。创建一个指针,若格式与指定的不同,可使用该指针处理原格式或转换为新格式。
NSPropertyListFormat propertyListFormat = NSPropertyListXMLFormat_v1_0;
NSString *error = nil;
NSMutableArray *capitols = [NSPropertyListSerialization propertyListFromData:data
mutabilityOption:NSPropertyListMutableContainersAndLeaves
format:&propertyListFormat
errorDescription:&error];
这里可以选择是否要修改 plist 以及是修改列表结构还是仅修改数据。
5. 对象编码机制
并非所有对象信息都能用属性列表类表示。好在 Cocoa 提供了让对象将自身转换为可保存到磁盘格式的机制,即对象编码和解码(也叫序列化和反序列化)。
之前在 Interface Builder 中,将对象从库拖到窗口,内容会保存到 nib 文件,这就是 NSWindow 和 NSTextField 对象的序列化和保存过程。程序运行加载 nib 文件时,对象会反序列化并重新创建和关联。
要对自己的对象进行同样操作,需采用
NSCoding
协议,协议如下:
@protocol NSCoding
- (void) encodeWithCoder: (NSCoder *) encoder;
- (id) initWithCoder: (NSCoder *) decoder;
@end
采用该协议需实现这两个方法。对象保存时调用
-encodeWithCoder:
,加载时调用
-initWithCoder:
。
NSCoder
是抽象类,定义了将对象与 NSData 相互转换的有用方法。实际使用其具体子类
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];
} // dealloc
- (void) encodeWithCoder: (NSCoder *) coder
{
// nobody home
} // encodeWithCoder
- (id) initWithCoder: (NSCoder *) decoder
{
return (nil);
} // initWithCoder
- (NSString *) description
{
NSString *description =
[NSString stringWithFormat: @"%@: %d/%.1f %@", name,
magicNumber, shoeSize, subThingies];
return (description);
} // description
@end // Thingie
在
main()
函数中创建并打印
Thingie
对象:
Thingie *thing1;
thing1 = [[Thingie alloc] initWithName: @"thing1" magicNumber: 42 shoeSize: 10.5];
NSLog (@"some thing: %@", thing1);
输出:
some thing: thing1: 42/10.5 ( )
接下来实现
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:
NSData *freezeDried;
freezeDried = [NSKeyedArchiver archivedDataWithRootObject: thing1];
再实现
-initWithCoder:
方法:
- (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);
} // initWithCoder
最后,释放原对象,从归档数据重新创建并打印:
[thing1 release];
thing1 = [NSKeyedUnarchiver unarchiveObjectWithData: freezeDried];
NSLog (@"reconstituted thing: %@", thing1);
输出与之前一致:
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 开发中的属性列表和对象编码为文件处理提供了强大而灵活的方式。通过属性列表,能方便地保存和读取简单数据结构;借助对象编码,可处理更复杂的自定义对象。掌握这些技术,能让我们在开发中更高效地管理数据和文件。
Cocoa 开发中的文件处理:属性列表与对象编码
6. 总结与应用建议
在 Cocoa 开发里,文件处理是重要的一部分,属性列表和对象编码为我们提供了有效的解决方案。下面总结关键要点并给出应用建议。
6.1 关键要点总结
- 属性列表类 :包含 NSArray、NSDictionary、NSString、NSNumber、NSDate 和 NSData 等,可方便地进行文件的读写操作。
- NSDate 和 NSData :NSDate 用于处理时间和日期,NSData 用于封装字节块,简化了数据传递和内存管理。
-
属性列表读写
:使用
-writeToFile:atomically:方法保存属性列表,+arrayWithContentsOfFile:方法读取属性列表。 -
对象修改
:借助
NSPropertyListSerialization类,能在保存和加载属性列表时获取错误信息,还可控制数据的可变性。 -
对象编码
:采用
NSCoding协议,结合NSKeyedArchiver和NSKeyedUnarchiver类,实现自定义对象的序列化和反序列化。
6.2 应用建议
- 简单数据存储 :若数据能表示为属性列表类型,优先使用属性列表的读写方法,能快速实现数据的保存和加载。
- 复杂对象处理 :对于自定义对象,采用对象编码机制,确保对象状态能正确保存和恢复。
-
错误处理
:使用
NSPropertyListSerialization类时,注意获取错误信息,便于调试和优化程序。 - 内存管理 :遵循 Cocoa 的内存管理规则,合理使用自动释放对象,避免内存泄漏。
7. 流程图展示
下面通过 mermaid 格式的流程图,展示属性列表和对象编码的处理流程。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B{数据类型}:::decision
B -->|属性列表类型| C(属性列表读写):::process
B -->|自定义对象| D(对象编码):::process
C --> E(保存到文件):::process
C --> F(从文件读取):::process
D --> G(实现 NSCoding 协议):::process
G --> H(使用 NSKeyedArchiver 归档):::process
G --> I(使用 NSKeyedUnarchiver 解档):::process
E --> J([结束]):::startend
F --> J
H --> J
I --> J
这个流程图清晰展示了根据数据类型选择不同处理方式的过程,有助于理解整个文件处理的流程。
8. 表格对比
为了更直观地对比属性列表和对象编码的特点,下面列出表格:
| 特性 | 属性列表 | 对象编码 |
| ---- | ---- | ---- |
| 适用数据类型 | 简单数据结构,如数组、字典、字符串等 | 自定义对象 |
| 读写方法 |
-writeToFile:atomically:
和
+arrayWithContentsOfFile:
| 实现
NSCoding
协议,使用
NSKeyedArchiver
和
NSKeyedUnarchiver
|
| 错误处理 | 不返回错误信息 | 可获取错误信息 |
| 数据可变性 | 读取后默认不可变,需额外处理 | 可根据需要控制可变性 |
9. 总结
通过对属性列表和对象编码的学习,我们掌握了在 Cocoa 开发中进行文件处理的有效方法。属性列表适用于简单数据的存储,操作简便;对象编码则能处理复杂的自定义对象,保证对象状态的完整保存和恢复。在实际开发中,根据数据的特点和需求,灵活选择合适的方法,能提高开发效率和程序的稳定性。
希望这些内容能帮助你更好地理解和应用 Cocoa 开发中的文件处理技术。在实践中不断探索和尝试,你将能熟练运用这些知识解决各种文件处理问题。
超级会员免费看
7

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



