Cocoa 文件处理:属性列表与对象编码全解析
在计算机编程的世界里,文件的保存和加载是一项基础且重要的功能。许多应用程序都会产生用户工作的半永久性成果,如编辑后的照片、小说章节等,这些最终都会以文件的形式保存下来。本文将深入探讨 Cocoa 框架中处理文件的两种主要方式:属性列表和对象编码。
1. 预备知识
在开始详细介绍文件处理之前,先简单了解一下相关的基础概念。在 Cocoa 中,有超过 100 个不同的类供开发者使用,其中一些在 Interface Builder 中可见。虽然标准 C 库提供了创建、读取和写入文件的函数调用,如
open()
、
read()
、
write()
、
fopen()
和
fread()
,但本文不会对其进行讨论。同时,Cocoa 提供的 Core Data 可以在幕后处理所有文件相关的操作,本文也不会涉及。
2. 属性列表
属性列表(Property Lists)是 Cocoa 中一类特殊的对象,通常缩写为 plist。这些列表包含一组 Cocoa 能够处理的对象,特别是可以将它们保存到文件中并在之后加载回来。属性列表类包括
NSArray
、
NSDictionary
、
NSString
、
NSNumber
、
NSDate
和
NSData
,以及它们的可变版本(如果有的话)。
2.1 NSDate
在程序中,时间和日期的处理非常常见。例如,iPhoto 知道你拍摄狗狗照片的日期,个人会计应用知道银行对账单的结算日期。
NSDate
是 Cocoa 中处理日期和时间的基础类。
NSDate *date = [NSDate date];
NSLog (@"today is %@", date);
上述代码会输出当前的日期和时间,格式类似于
2012-01-23 11:32:02 -0400
。此外,还可以使用
+dateWithTimeIntervalSinceNow:
方法来获取相对于当前时间的日期,例如获取 24 小时前的日期:
NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow: -(24 * 60 * 60)];
NSLog (@"yesterday is %@", yesterday);
如果你想格式化日期的输出,Apple 提供了
NSDateFormatter
类,它符合 Unicode 技术标准 #35,可以为用户提供各种日期格式。
2.2 NSData
在 C 语言中,常见的做法是将数据缓冲区传递给函数,通常需要传递缓冲区的指针和长度,并且可能会出现内存管理问题。Cocoa 提供了
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);
上述代码创建了一个
NSData
对象来保存一个普通的 C 字符串,并打印出数据。输出结果可能是一串十六进制字符,如
<48692074 68657265 2c207468 69732069 73206120 43207374 72696e67 2100>
。通过 ASCII 表可以将其转换为对应的字符串。
NSData
对象是不可变的,一旦创建就不能更改。不过,
NSMutableData
允许你添加和删除数据内容中的字节。
2.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 代码:
<?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>
要读取这个文件,可以使用
+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:
参数是一个安全机制,它会先将文件内容保存到临时文件中,保存成功后再将临时文件与原文件交换。除非你要保存的文件非常大,可能会填满用户的硬盘,否则建议以原子方式保存文件。
2.4 修改对象
当使用集合类型从文件中读取数据时,默认情况下不能修改数据。一种修改数据的方法是使用暴力方式,遍历 plist 并创建一个可变的并行结构。但实际上,有更简单的方法。
NSPropertyListSerialization
类可以根据你的需求保存和加载属性列表。
NSString *error = nil;
NSData *encodedArray = [NSPropertyListSerialization dataFromPropertyList:capitols
format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
[encodedArray writeToFile:@"/tmp/capitols.txt" atomically:YES];
上述代码将 plist 数据以二进制格式写入文件。要将数据读回内存,需要指定文件格式:
NSPropertyListFormat propertyListFormat = NSPropertyListXMLFormat_v1_0;
NSString *error = nil;
NSMutableArray *capitols = [NSPropertyListSerialization propertyListFromData:data
mutabilityOption:NSPropertyListMutableContainersAndLeaves
format:&propertyListFormat
errorDescription:&error];
在读取数据时,可以选择是否要修改 plist 以及是否要修改列表的结构或仅修改数据。
3. 总结
属性列表是一种方便的方式来保存和加载数据,特别是当你的数据可以归结为属性列表类型时。使用属性列表可以快速启动和运行程序,并且可以使用
NSData
来简化保存数据块的工作。但需要注意的是,这些函数不会返回任何错误信息,如果无法加载文件,只会返回一个
nil
指针。
通过以下流程图可以更直观地了解属性列表的读写过程:
graph LR
A[创建属性列表对象] --> B[写入文件]
B --> C{写入成功?}
C -- 是 --> D[文件保存完成]
C -- 否 --> E[处理错误]
F[读取文件] --> G{读取成功?}
G -- 是 --> H[加载属性列表对象]
G -- 否 --> I[处理错误]
属性列表的相关操作总结如下表:
| 操作 | 方法 | 说明 |
| ---- | ---- | ---- |
| 写入文件 |
-writeToFile:atomically:
| 将属性列表写入文件,
atomically:
参数是安全机制 |
| 读取文件 |
+arrayWithContentsOfFile:
| 从文件中读取属性列表 |
| 数据转换 |
NSPropertyListSerialization
相关方法 | 保存和加载属性列表,可指定格式和可变性选项 |
Cocoa 文件处理:属性列表与对象编码全解析
4. 编码对象
并非所有对象的信息都能以属性列表类的形式表达。如果所有信息都能用数组的字典来表示,就不需要自定义类了。幸运的是,Cocoa 提供了一种机制,让对象能够将自身转换为可保存到磁盘的格式。对象可以将其实例变量和其他数据编码成一块数据,这块数据可以保存到磁盘,之后再读回内存并基于保存的数据创建新对象。这个过程称为编码和解码,或序列化和反序列化。
要让自定义对象支持编码和解码,需要采用
NSCoding
协议。该协议定义了两个方法:
@protocol NSCoding
- (void) encodeWithCoder: (NSCoder *) encoder;
- (id) initWithCoder: (NSCoder *) decoder;
@end
通过采用这个协议,需要实现这两个方法。当对象被要求保存自身时,会调用
-encodeWithCoder:
方法;当对象被要求加载自身时,会调用
-initWithCoder:
方法。
NSCoder
是一个抽象类,定义了一些将对象转换为
NSData
并转换回来的有用方法。实际上不会创建
NSCoder
实例,而是使用它的具体子类
NSKeyedArchiver
和
NSKeyedUnarchiver
来编码和解码对象。
下面通过一个示例来演示如何使用这些类。首先定义一个简单的类
Thingie
:
@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 *freezeDried;
freezeDried = [NSKeyedArchiver archivedDataWithRootObject: thing1];
可以将这个
NSData
保存到磁盘,这里先释放
thing1
对象,然后从归档数据中重新创建它并打印:
[thing1 release];
thing1 = [NSKeyedUnarchiver unarchiveObjectWithData: freezeDried];
NSLog (@"reconstituted thing: %@", thing1);
输出结果与之前相同:
reconstituted thing: thing1: 42/10.5 ( )
还可以向
thing1
的
subThingies
数组中添加其他
Thingie
对象:
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 (
)
)
5. 总结
通过属性列表和对象编码,Cocoa 提供了强大而灵活的文件处理能力。属性列表适用于数据可以归结为特定类型的情况,操作简单方便;而对象编码则允许自定义对象进行序列化和反序列化,满足更复杂的需求。
对象编码的主要步骤总结如下列表:
1. 让自定义类采用
NSCoding
协议。
2. 实现
-encodeWithCoder:
方法,将对象的实例变量编码到
NSCoder
中。
3. 实现
-initWithCoder:
方法,从
NSCoder
中解码实例变量。
4. 使用
NSKeyedArchiver
进行对象归档,得到
NSData
。
5. 使用
NSKeyedUnarchiver
从
NSData
中恢复对象。
以下是对象编码和解码过程的流程图:
graph LR
A[创建对象] --> B[编码对象]
B --> C[生成 NSData]
C --> D[保存到磁盘]
E[从磁盘读取 NSData] --> F[解码对象]
F --> G[恢复对象]
对象编码相关操作总结如下表:
| 操作 | 方法 | 说明 |
| ---- | ---- | ---- |
| 编码对象 |
-encodeWithCoder:
| 将对象的实例变量编码到
NSCoder
中 |
| 解码对象 |
-initWithCoder:
| 从
NSCoder
中解码实例变量 |
| 归档对象 |
+archivedDataWithRootObject:
| 将对象归档为
NSData
|
| 恢复对象 |
+unarchiveObjectWithData:
| 从
NSData
中恢复对象 |
通过掌握这些技术,可以在 Cocoa 应用程序中高效地处理文件的保存和加载,为用户提供更好的体验。
超级会员免费看
74

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



