29、Cocoa 文件处理:属性列表与对象编码全解析

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 应用程序中高效地处理文件的保存和加载,为用户提供更好的体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值