文件加载与保存
- 大多数计算机程序(应用程序)在关闭时都会为用户的当前成果创建一个临时的(非永久的)文件,可能是小说的某个章节,或是某个乐队专辑的封面。但无论是那种情形,用户都会有个文件保存在磁盘上。
- 标准C函数库提供了函数调用来创建,读取和写入文件,例如open(),read(),write(),fopen()和fread()。而Cocoa提供了Core Data,它能在后台进行文件的以上所有操作。
- Cocoa提供了两个处理文件的通用类:属性对象和对象编码。
1.属性列表
在Cocoa中,有一类名为属性列表(property list)的对象,通常简写为plist。这些属性列表类是NSArray,NSDictionary,NSString,NSNumber,NSDate和NSData(前面四个已经讲过),以及它们的可修改形态(只要它们能拥有前缀为Mutable的类)
1.1NSDate
- 程序中经常要处理时间和日期。NSDate类是Cocoa中用于处理日期和时间的基础(Foundation)类。
- 可以使用[NSDate data]来获取当前的日期和时间,它会返回一个能自动释放的对象。
NSDate *date = [NSDate date];
NSLog(@"today is %@",date);
//输出结果
today is 2018-12-01 20:21:02 -0400
- 你可以使用一些方法比较两个日期,从而对列表进行排序,还可以获取与当前时间间隔一定时差的日期。
//获取24小时之前的确切时间
NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow:-(24*60*60)];
NSLog(@"yesterday is %@",yesterday);
//输出结果
yesterday is 2018-11-30 20:21:02 -0400
- +dateWithTimeIntervalSinceNow:接受一个NSTimeInterval参数,该参数是一个双精度值,表示以妙为单位计算的时间间隔。通过该参数可以指定时间偏移的方式:对于将来的时间,使用时间间隔的整数;对于过去的时间,使用时间间隔的负数。
- 如果你想要设定输出结果的时间格式,苹果公司提供了一个叫做NSDateFormatter的类,它符合35号Unicode技术标准,能为用户提供多种时间的显示格式。
1.2NSData
- 将缓冲区(buffer)的数据传递给函数是C语言中常见的操作。通常是将缓冲区的指针和长度传递给某个函数。另外,C语言中可能会出现内存管理问题。例如,如果缓冲区已经被动态分配,那么当它不在使用时,由谁负责将其清除?
- Cocoa提供了NSData类,该类可以包含大量字节。你可以获取数据的长度和指向字节和起始位置的指针。因为NSData是一个对象,所以常规的内存管理对它是有效的。因此,如果想将数据块传递给一个函数或方法,可以通过传递一个支持自动释放的NSData来实现,而无需担心内存清理的问题。下面的NSData对象将保存一个普通的C字符串(一个字节序列),然后输出数据。
const char *string = "Hi there,this is a C string!";
NSData *data = [NSData dataWithBytes: length: strlen(string) + 1];
NSLog(@"data is %@",data);
//运行结果
data is <48692074 68657265 2c207468 69732069 73206120 43207374 72696e67 2100>
- 上面的输出有点特别,但是如果你有ASCII表(打开终端,并键入命令man ascii就可以找到该表),就可以看到,这个十六进制数据块就是我们的字符串,0×48表示字符H,0×69表示字符i等。-length方法给出字节数量,-byte方法给出指向字符串起始位置的指针。注意到+dataWiithBytes:调用中的+1了吗?它用于C语言字符串所需的尾部的零字节。还需注意NSlog输出结果末尾的00。通过包含零字节,就可以使用%s格式的说明符输出字符串。
NSLog(@"%d byte string is "%s",[data length],[data bytes]);
//输出结果
30 byte is "Hi there,this is a C string!"
- NSData对象是不可变更的,创建后就不可以改变。你可以使用它们,但不能更改其中的内容。不过NSMutableData支持在数据内容中添加和删除字节。
1.3写入和读取属性列表
- 集合属性列表类(NSArray和NSDictionary)具有一个-writeToFile:atomically:方法,用于将属性列表的内容写入文件。
- NSString和NSData也具有-writeToFile:atomically:方法,不过只能写出字符串或者数据块。因此,我们可以将字符串存入一个数组,然后保存它。
相关例子代码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
NSArray *phrase;
phrase = [NSArray arrayWithObjects:@"I",@"seem",@"to",@"be",@"a",@"verb",nil];
[phrase writeToFile:@"/Users/xulei/Desktop/file.txt" atomically:YES];
return 0;
}
运行结果:
以上代码虽然有些繁琐,但正是我们要保存的内容:一个字符串数组。如果保存的数组是包含了各种字符串数组,数字和数据的字典,那么这些属性列表文件的内容将相当复杂。Xcode也包含了一个属性列表编辑器,所以你可以查看plist文件并进行编辑。
有些属性列表文件,特别是首选项文件,是以压缩的二进制格式存储的。通过使用plutil命令:plutil -convert xml1文件名.plist,可以将这些文件转化成人可以理解的字面形式。
现在我们的磁盘中已经有了verbiage.txt文件,可以使用+arrayWithContentsOfFile:方法读取该文件。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
NSArray *phrase1;
phrase1 = [NSArray arrayWithObjects:@"I",@"seem",@"to",@"be",@"a",@"verb",nil];
[phrase1 writeToFile:@"/Users/xulei/Desktop/file.txt" atomically:YES];
NSArray *phrase2 = [NSArray arrayWithContentsOfFile:@"/Users/xulei/Desktop/file.txt"];
NSLog(@"%@",phrase2);
return 0;
}
//运行结果
2018-12-18 12:47:02.350581-0800 OC文件[632:19018] (
I,
seem,
to,
be,
a,
verb
)
Program ended with exit code: 0
- 注意到writeToFile:方法中的atomically了吗?atomically:参数的值为BOOL类型,它会告诉Cocoa是否应该首先将文件内容保存在临时文件中,再将该临时文件和原始文件交换。
- 这是一种安全机制:如果在保存过程中出现意外,不会破坏原始文件。但这种安全机制需要付出一定的代价:在保存过程中,由于原始文件仍然保存在磁盘中,所以需要使用双倍的磁盘空间。你应该尽量使用atomically的方式保存文件,除非保存的文件容量非常大,会占用用户大量的硬盘空间。
- 如果能将数据归结为属性列表类型,则可以使用这些非常便捷的方法调用来将内容保存到磁盘中,供以后读取。
- 这些函数的一个缺点是不会返回任何错误信息。如果无法加载文件,你只能从方法中获得一个nil指针,但无法得知出现错误的具体原因。
1.4修改对象类型
- 需要注意,当使用集合类从某文件读取数据时,你无法修改数据的类型。一种解决方法是强制转换,遍历plist文件的内容并创建一个平行结构的可修改对象。不过还有一种方法,需要用到类NSPropertyListSerialization,正如名字所提示的,它可以为存储和加载属性列表的行为添加很多你需要的设定项。
- 尤其要注意propertyListFromData:mutabilityOption:format:errorDescription:方法。它能把plist数据返回给你,并且能在出现异常的时候提供错误信息。
- 以下是将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 propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:&propertyListFormat error:&error];
- 其中一个选项就是以什么方式来读取数据:我们是否想要能够修改plist文件的类型?我们是想获取列表结构还是仅获取二进制数据?
- 以下是将属性列表文件plist数据内容以二进制形式写入文件的代码:
void writeCapitols (void)
{
NSMutableArray *capitols = [NSMutableArrayarrayWithCapacity:10];
NSMutableDictionary *capitol = [NSMutableDictionarydictionaryWithObject:@"Canada"forKey:@"country"];
[capitol setObject:@"Ottawa"forKey:@"capitol"];
[capitols addObject:capitol];
capitol = [NSMutableDictionarydictionaryWithObject:@"Norway"forKey:@"country"];
[capitol setObject:@"Oslo"forKey:@"capitol"];
[capitols addObject:capitol];
NSString *error =nil;
NSData *encodedArray = [NSPropertyListSerializationdataFromPropertyList:capitols
format:NSPropertyListBinaryFormat_v1_0
errorDescription:&error];
[encodedArray writeToFile:@"/tmp/capitols.txt"atomically:YES];
}
如你所见,我们将数组数据转换成了NSData类型并写入了文件中。
将数据读取回内存要多执行一步,即指定文件的类型。我们创建了一个指针,如果文件格式与指定的类型不同,可以换用原格式类型的指针,也可以将读取的内容转换成新的格式。
static void modifyCapitols(void)
{
NSData *data = [NSDatadataWithContentsOfFile:@"/tmp/capitols.txt"];
NSPropertyListFormat propertyListFormat =NSPropertyListXMLFormat_v1_0;
NSString *error =nil;
NSMutableArray *capitols = [NSPropertyListSerializationpropertyListFromData:data
mutabilityOption:NSPropertyListMutableContainersAndLeaves
format:&propertyListFormat
errorDescription:&error];
NSLog(@"capitols %@", capitols);
}
- 在main函数中,我们调用writeCapitols();和modifyCapitols();的输出结果如下:
capitols (
{
capitol = Ottawa;
country = Canada;
},
{
capitol = Oslo;
country = Norway;
}
)
- 可以使用[NSDate date]来获取当前的日期和时间,它会返回一个能自动释放的对象。
NSDate *date = [NSDatedate];
NSLog (@"today is %@", date);
将输出的结果为:
today is 2013-12-4 19:58:06 -0400。
- 有些属性列表文件,特别是首选项文件,是以压缩的二进制格式存储的。通过使用plutil命令:plutil - convert xml1文件名.plist,可以将这些文件转换成人可以理解的字面形式。
2.编码对象
- 不幸的是,你无法总是将对象信息表示为属性列表类。
- Cocoa具备一种将对象转换成某种格式并保存到磁盘中的机制。对象可以将它们的实例变量和其他数据编码为数据块,然后保存到磁盘中。这些数据块以后还可以读回内存中,并且还能基于保存的数据创建新对象。这个过程被称为编码与解码(encoding and decoding),也可以叫做序列化与反序列化(serialization and deserialization)。
- 前面说Interface Builder时,我们从库中将对象拖到窗口,这些对象会被存储到nib文件中。换句话说,NSWindow和NSTextField对象都被序列化并保存到磁盘中。当程序运行时,会将nib文件加载到内存中,对象会被反序列化,新的NSWindow与NSTextField对象会被创建并建立关系。
- 通过采用NSCoding协议,可以使自己的对象实现相同的功能。该协议与下面的代码类似。
@protocol NSCoding
- (void) encodeWithCoder:(NSCoder *) encoder;
- (id) initWithCoder:(NSCoder *) decoder;
@end
- 通过采用该协议,可以实现两种方法。当对象需要保存自身时,就会调用-encodeWithCoder:方法;当对象需要加载自身时,就会调用-initWithCoder:方法。
- NSCoder是一个抽象类,它定义了一些有用的方法,便于对象与NSData之间的转换,你完全没必要创建一个新的NSCoder对象,因为实际上并不会起作用。不过有一些具体实现的NSCoder子类可以用来编码和解码对象,我们使用其中的两个子类NSKeyedArchiver和NSKeyedUnarchiver。
#import <Foundation/Foundation.h>
//包含一些实例变量的简单类
@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类采用了NSCoding协议这意味着我们将要实现encodeWithCoder:方法和initWithCoder:方法
//先将这两个方法的内容置空
//该代码初始化了一个新的对象,清除了我们创建的所有东西,并且为NSCodering协议的方法创建了结构框架以避免编译器报错,最后返回了描述信息的方法。
@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{
}
//现在,我们来将这个对象归档
//我们使用NSKeyedArchiver把对象归档到NSData中,KeyedArchiver使用键/值来保存对象的信息
//Thingie的encodeWithCoder方法会使用与每个实例变量名称匹配的键对其进行编码
//也可以使用无规则的文字作为键来编码,保证键的名称与实例变量的名称相似有助于识别它们之间的映射关系
//可以使用上面的字面量字符串作为编码建
//可以使用#define kSubthingiesKey @"subThingies"来定义常量
//可以使用文件的局部变量,比如static NSString *kSubthingiesKey = @"subThingies";所定义的静态变量
//注意,每种类型的encodeSomething:forKey:方法都不同。需要确保你的类型使用的是正确的编码方法。
//对于所有的Objective-C对象类型,都要使用encodeObjective:forKey:方法。
-(void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:name forKey:@"name"];
[aCoder encodeInt:magicNumber forKey:@"magicNumber"];
[aCoder encodeFloat:shoeSize forKey:@"shoeSize"];
[aCoder encodeObject:subThingies forKey:@"subThingies"];
}
//如果需要恢复某个归档的对象,可以使用decodeSomethingForKey方法
//initWithCoder:和其它的init:方法一样,在为对象执行操作之前,需要使用超类进行初始化
//可以采用两种方式,具体取决于父类
//如果父类采用了NSCoding协议,则应该调用[super initWithCoder:aDecoder],否则只需要调用[super init]
//当你使用decodeIntForKey:方法时,会把一个init值从aDecoder中取出
//当你使用decodeObjectForKey:方法时,会把一个对象从aDecoder中取出
//如果里面还有嵌入的对象,就会对其递归调用initWithCoder:方法。
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self =[super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.magicNumber = [aDecoder decodeIntForKey:@"magicNumber"];
self.shoeSize = [aDecoder decodeFloatForKey:@"shoeSize"];
self.subThingies = [aDecoder decodeObjectForKey:@"subThingies"];
}
return self;
}
-(NSString *)description{
NSString *description = [NSString stringWithFormat:@"%@: %d/%.1f %@",name,magicNumber,shoeSize,subThingies];
return description;
}
@end
//你将会注意到,编码和解码的顺序和示例对象的顺序完全相同。当然,你也可以不这么做,这只是一种简便的习惯
//目的在于确保每个对象都进行了编码和解码操作,没有对象被漏掉
//使用键进行调节的原因之一就是可以将示例对象按任意顺序放入或取出
int main(int argc, const char * argv[]) {
Thingie *thing;
thing = [[Thingie alloc] initWithName:@"thing1" magicNumber:42 shoeSize:10.5];
NSLog(@"some thing:%@",thing);
//使用thing1对象,并将其归档
NSData *freezeDried;
//类方法+archivedDataWithRootObject:对这个对象进行了编码,比如字符串、数组及放入数组中的任何对象
//当所有的对象完成了键值编码后,会被放入一个NSData并返回
freezeDried = [NSKeyedArchiver archivedDataWithRootObject:thing];
Thingie *thing1;
thing1 = [NSKeyedUnarchiver unarchiveObjectWithData:freezeDried];
NSLog(@"%@",thing1);
//我们可以将对象放到subThing的可变数组中,当数组被编码时,这些对象将被自动编码
//NSArray中encodeWithCoder:方法的实现会对所有的对象调用encodeWithCoder方法,直到所有对象都被编码
Thingie *anotherThing;
anotherThing = [[Thingie alloc] initWithName:@"thing2" magicNumber:23 shoeSize:13.0];
[thing1.subThingies addObject:anotherThing];
anotherThing = [[Thingie alloc] initWithName:@"thing3" magicNumber:17 shoeSize:9.0];
[thing1.subThingies addObject:anotherThing];
NSLog(@"%@",thing1);
//编码解码的工作机制是一样的
freezeDried = [NSKeyedArchiver archivedDataWithRootObject:thing1];
thing1 = [NSKeyedUnarchiver unarchiveObjectWithData:freezeDried];
NSLog(@"%@",thing1);
return 0;
}
//运行结果
2018-12-21 06:26:32.998091-0800 编码对象[1126:102536] some thing:thing1: 42/10.5 (
)
2018-12-21 06:26:32.999011-0800 编码对象[1126:102536] thing1: 42/10.5 (
)
2018-12-21 06:26:32.999227-0800 编码对象[1126:102536] thing1: 42/10.5 (
"thing2: 23/13.0 (\n)",
"thing3: 17/9.0 (\n)"
)
2018-12-21 06:26:32.999503-0800 编码对象[1126:102536] thing1: 42/10.5 (
"thing2: 23/13.0 (\n)",
"thing3: 17/9.0 (\n)"
)
Program ended with exit code: 0
我们使用NSKeyedArchiver把对象归档到NSData中,KeyedArchiver使用键/值来保存对象的信息。
Thingie的encodeWithCoder方法会使用与每个实例变量名称匹配的键对其进行编码。
也可以使用无规则的文字作为键来编码,保证键的名称与实例变量的名称相似有助于识别它们之间的映射关系。
可以使用上面的字面量字符串作为编码建。
可以使用#define kSubthingiesKey @"subThingies"来定义常量。
可以使用文件的局部变量,比如
static NSString *kSubthingiesKey = @"subThingies";
所定义的静态变量。注意,每种类型的encodeSomething:forKey:方法都不同。需要确保你的类型使用的是正确的编码方法。
对于所有的Objective-C对象类型,都要使用encodeObjective:forKey:方法。
如果需要恢复某个归档的对象,可以使用decodeSomethingForKey方法
initWithCoder:和其它的init:方法一样,在为对象执行操作之前,需要使用超类进行初始化。可以采用两种方式,具体取决于父类.如果父类采用了NSCoding协议,则应该调用[super initWithCoder:aDecoder],否则只需要调用[super init]。
当你使用decodeIntForKey:方法时,会把一个init值从aDecoder中取出;当你使用decodeObjectForKey:方法时,会把一个对象从aDecoder中取出,如果里面还有嵌入的对象,就会对其递归调用initWithCoder:方法。
你将会注意到,编码和解码的顺序和示例对象的顺序完全相同。当然,你也可以不这么做,这只是一种简便的习惯。目的在于确保每个对象都进行了编码和解码操作,没有对象被漏掉。使用键进行调节的原因之一就是可以将示例对象按任意顺序放入或取出。
如果编码的数据中含有循环会怎么样?例如thing1就在自己的subThingies数组中会怎么样?会不停的重复编码吗?Cocoa的归档实现非常智能,对象循环也可以进行保存或恢复。
如果要进行实验的话,可以将thing1放入其自身的subThingies数组中。但是不要尝试在thing1中使用NSLog,NSLog不够智能,不能检测对象循环。因此,它将会陷入一个无限的递归,试图创建日志信息,最终会导致成千上万的-description调用进入调试器中。不过,如果对thing1进行编码和解码,它可以完美的运行,也不会陷入混乱中。