29、Cocoa 开发中的文件处理:属性列表与对象编码

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 开发中的文件处理技术。在实践中不断探索和尝试,你将能熟练运用这些知识解决各种文件处理问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值