More Related Content Similar to 14 Saving Loading and Application States
Similar to 14 Saving Loading and Application States (20) 14 Saving Loading and Application States2. • 在⼀一个 iOS application 中保存和加载数据有很多
⽅方法。
• 这⼀一个主题我们来看⼀一下⼀一些最常⻅见的机制,以
及在 iOS 上对⽂文件系统进⾏行读写需要理解的概念
4. • 任何 iOS 应⽤用实际都在做⼀一件事情:提供给⽤用户
⼀一个界⾯面让他们来操作数据。
• 在应⽤用中的每⼀一个对象在这个过程中都扮演下⾯面
的⼀一个⾓角⾊色。
• Model 对象,负责持有⽤用户操作的数据
• View 对象,只是体现这些数据
• Controllers,负责应⽤用运⾏行到底是怎么回事
• 因此当我们讨论保存和加载数据时,我们⼏几乎总
是在谈论保存和加载 Model 对象
5. • 在 Homepwner 中,⽤用户操作的模型对象是
BNRItem 的实例。
• 如果在应⽤用退出后再启动时 BNRItem 的实例能够
持久存在,Homepwner 将真正成为⼀一个有⽤用的应
⽤用
• 在这⼀一章,我们将使⽤用 archiving 来保存和加载
BNRItem
6. • Archiving 是在 iOS 上持久化模型数据的⼀一种最常
⻅见的⽅方法。
• Archiving ⼀一个对象会记录它所有的实例变量并且
把他们保存到⽂文件系统。
• Unarchiving ⼀一个对象就是从⽂文件系统加载数据,
并且再从这个记录⽣生成对象。
7. NSCoding
• 需要把它的实例进⾏行 archive 和 unarchive 的类必
须符合 NSCoding protocol。
• 并且实现它的两个必须的⽅方法,
encodeWithCoder: 和 initWithCoder:
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;
8. encode
• 在 BNRItem.h 中增加 NSCoding protocol 的声明
@interface BNRItem : NSObject <NSCoding>
• 然后是实现两个必须的⽅方法,⾸首先是
encodeWithCoder:
9. • 当 BNRItem 被发送 encodeWithCoder: 消息时,它
将把它的所有的实例变量编码到作为⼀一个参数传
⼊入的 NSCoder 对象中
• 我们可以把 NSCoder 对象想成⼀一个数据容器,负
责组织这些数据
• NSCoder 以key-value pair 的⽅方式组织数据
10. encodeWithCoder:
• 在 BNRItem.m 中实现 encodeWithCoder: 以添加实
例变量到这个容器
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:itemName forKey:@"itemName"];
[aCoder encodeObject:serialNumber forKey:@"serialNumber"];
[aCoder encodeObject:dateCreated forKey:@"dateCreated"];
[aCoder encodeObject:imageKey forKey:@"imageKey"];
[aCoder encodeInt:valueInDollars forKey:@"valueInDollars"];
}
15. initWithCoder:
• encoding 时使⽤用 key 的⺫⽬目的是为了之后在
BNRItem 被从⽂文件系统加载时⽤用来获取已编码的
值
• 被从⼀一个 archive 中加载的对象被发送的是
initWithCoder: 消息。
• 这个⽅方法攫取所有在 encodeWithCoder: 中被编码
的对象,并且把它们分配给合适的实例变量。
• 在 BNRItem.m 中实现这个⽅方法
16. decodeXXXForKey:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
[self setItemName:[aDecoder decodeObjectForKey:@"itemName"]];
[self setSerialNumber:[aDecoder
decodeObjectForKey:@"serialNumber"]];
[self setImageKey:[aDecoder decodeObjectForKey:@"imageKey"]];
[self setValueInDollars:[aDecoder
decodeIntForKey:@"valueInDollars"]];
dateCreated = [aDecoder decodeObjectForKey:@"dateCreated"];
}
return self;
}
• 这个⽅方法也有⼀一个 NSCoder 参数,它⾥里⾯面的数据
是供正在被初始化的 BNRItem 使⽤用的
• 使⽤用 decodeObjectForKey: 取回对象(object),
decodeIntForKey: 取回 valueInDollar
17. • 经过前⾯面的改造之后,BNRItem 现在是 NSCoding
兼容的了,⽽而且也可以使⽤用 archiving 从⽂文件系统
进⾏行保存和加载。
• 现在还有两个问题要考虑:
• 我们需要⼀一种⽅方法来开启保存和加载操作?(现在只
是可以 archiving 和 unarchiving)
• 另外,我们需要在⽂文件系统找⼀一个地⽅方来存储我们要
保存的 BNRItem
19. • 每⼀一个 iOS 应⽤用都有它⾃自⼰己的 applicaton
sandbox(应⽤用沙箱)。
• ⼀一个 application sandbox 是在⽂文件系统上的⼀一个
和⽂文件系统其余部分隔离的⺫⽬目录。
• 应⽤用必须位于这个 sandbox 内,并且没有其他的
应⽤用可以访问你的 sandbox。
26. 构造⼀一个⽂文件路径
• 来⾃自 Homepwner 的 BNRItem 将被保存到
Documents ⺫⽬目录中⼀一个单独的⽂文件中
• BNRItemStore 将处理⽂文件写⼊入和读取的⼯工作
• ⾸首先,我们需要来构建这个⽂文件的路径
• 在 BNRItemStore 中声明和实现⼀一个新的⽅方法
27. - (NSString *)itemArchivePath
{
NSArray *documentDirectories =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,
YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
return [documentDirectory
stringByAppendingPathComponent:@"items.archive"];
}
• NSSearchPathForDirectoriesInDomains 函数搜索
⽂文件系统来查找符合给定参数标准的路径
• 在 iOS 上,最后两个参数总是⼀一样的
• 第⼀一个参数是指定了在 sandbox 中⺫⽬目录的常量。
⽐比如搜索 NSCachesDirectory 将会返回在应⽤用的
sandbox 中的 Caches ⺫⽬目录
• 返回值是⼀一个字符串数组,在 iOS 上只有⼀一个,
所以只取第⼀一个就可以了
29. • 现在我们已经有了⼀一个⽂文件系统的路径,同时模
型对象(BNRItem)也可以被保存到⽂文件系统了
• 我们可以使⽤用 NSKeyedArchiver 在应⽤用 “退出” 时
保存 BNRItems
• 在 BNRItemStore.h 中声明⼀一个新的⽅方法:
- (BOOL)saveChanges;
30. archiveRootObject:
- (BOOL)saveChanges
{
// 返回成功或失败
NSString *path = [self itemArchivePath];
return [NSKeyedArchiver archiveRootObject:allItems toFile:path];
}
• 在 BNRItemStore.m 中实现这个⽅方法:发送
archiveRootObject:toFile: 消息到
NSKeyedArchiver 类
• archiveRootObject:toFile: ⽅方法负责保存 allItems 中
每个单独的 BNRItem 到 itemArchivePath
32. • allItems 收到 encodeWithCoder: 消息后,会再发
给它包含的所有的对象,传递的同样是
NSKeyedArchiver。
• 这个数组的内容,也就是⼀一堆 BNRItems,再
encode 它们的实例变量到同⼀一个
NSKeyedArchiver。
• ⼀一旦所有这些对象都被编码之后,
NSKeyedArchiver 就把它收集到的这些数据写到它
的 path 参数(我们前⾯面构建的路径)
34. applicationDidEnterBackGround:
• 当⽤用户点击 “Home” 时,
applicationDidEnterBackground: 消息被发给
HomepwnerAppDelegate,我们希望在这时候给
BNRItemStore 发送 saveChanges
• 我们在 HomepwnerAppDelegate.m 中修改
applicationDidEnterBackGround: 来启动 BNRItems
的保存(别忘了导⼊入 BNRItemStore 头⽂文件)
- (void)applicationDidEnterBackground:(UIApplication *)application
{
BOOL success = [[BNRItemStore defaultStore] saveChanges];
if (success) {
NSLog(@"保存所有的 BNRItem ");
} else {
NSLog(@"⽆无法保存 BNRItem ");
}
}
35. ⽂文件写⼊入确认
• 构建并在模拟器中运⾏行,创建⼀一个新的
BNRItems。然后点击 home 键离开应⽤用,检查控
制台,我们应该看到前⾯面代码中记录的⽇日志。
• 我们也可以在电脑的⺫⽬目录中确认⼀一下⽂文件是否写
⼊入成功。在 Finder 中,Command-Shift-G, 键⼊入“~/
Library/Application Support/iPhone Simulator”,
找到应⽤用的 sandbox 查看⼀一下
37. unarchiveObjectWithFile:
- (id)init
{
self = [super init];
if (self) {
// allItems = [[NSMutableArray alloc] init];
NSString *path = [self itemArchivePath];
allItems = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
// 如果数组之前没有被保存,创建⼀一个新的空数组
if (!allItems) {
allItems = [[NSMutableArray alloc] init];
}
}
return self;
}
• unarchiveObjectWithFile: ⽅方法创建⼀一个
NSKeyedUnarchiver 实例加载位于 itemArchivePath
的 archive 到它的实例
38. • 下⾯面我们来看⼀一下加载的过程。
• 检测 archive 中 root 对象的类型并且⽣生成这个类型
的实例,在这⾥里,这个类型将是⼀一个
NSMutableArray,因为我们就是使⽤用这个类型的
root object 来⽣生成的这个 archive
39. • 然后新分配的 NSMutableArray 会被发送
initWithCoder: , NSKeyedUnarchiver 被作为参数传
⼊入;
• 然后就是数组开始从 NSKeyedUnarchiver 解码它
的内容( BNRItem 的实例), 给这些对象发送
initWithCoder: 消息,传递同样的
NSKeyedUnarchiver
• 再次构建并运⾏行,测试⼀一下加载功能
40. 不⽣生成随机数据了
• 因为我们现在已经可以保存和加载 BNRItem 了,
所以我们可以把⽣生成随机数据的功能删除掉。
• 在 BNRItemStore.m 中修改 createItem 的实现让它
⽣生成⼀一个不带随机数据的空的 BNRItem
- (BNRItem *)createItem
{
// BNRItem *p = [BNRItem randomItem];
BNRItem *p = [[BNRItem alloc] init];
[allItems addObject:p];
return p;
}
42. • 在 Hompwner 中,我们是在应⽤用程序进⼊入 backgroud
state 时 archive BNRItem 的。
• 下⾯面我们来看⼀一下应⽤用程序状态有关的内容,包括这
些状态是怎么被触发的,以及我们怎么获得通知
• 后⾯面是⼀一个有关状态的概要图
46. inactive state
• 在 active state 下,应⽤用可以被系统事件临时性的
打断,例如 SMS 消息,推送通知,来电,或者
alarm。应⽤用上⾯面会被叠加⼀一个界⾯面来处理这个事
件。这种状态被称为 inactive state 。
47. • 在 inactive state 下,应⽤用⼤大部分是可⻅见的,并且
在执⾏行代码,但是没有在接收事件。应⽤用⼀一般在
inactive state 下会停留很短的⼀一个时间。
• 可以通过按下设备顶部的锁定按钮(lock)让活
动的应⽤用程序强制进⼊入 inactive state
• 直到设备被解锁(unlock)前,应⽤用会⼀一直保持
inactive
48. background state
• 当⽤用户按下 home 键或是某种其他⽅方式切换到其
它应⽤用时,应⽤用进⼊入 background state。
• 在 background state 下,界⾯面不可⻅见,也不能接
收消息,但是仍然可以执⾏行代码。
• 默认情况下,进⼊入 background state 的应⽤用在进
⼊入 suspended state 前有 5 秒的时间。
51. 应⽤用状态表
Receives Executes
State Visible
Events Code
Not Running No No No
Active Yes Yes Yes
Inactive Mostly No Yes
Background No No Yes
Suspended No No No
53. • 我们应该在往 background state 转换时保存关键
的修改和应⽤用程序的状态。
• 因为这是应⽤用程序在进⼊入 suspended state 前还可
以执⾏行代码的最后时刻。
• ⼀一旦进⼊入了 suspended state,应⽤用有可能会被操
作系统根据⾃自⾝身的需要终⽌止。
55. • 对 Homepwner 进⾏行 archiving 时我们保存和加载
了针对每个 BNRItem 的 imageKey。
• 我们可以扩展 image store,在它们被添加时保存
图⽚片,然后在需要的时候可以提取出来。
56. • BNRItem 实例的 image 是通过⽤用户交互⽣生成的,
并且只存储在应⽤用程序内部。
• 这样 Documents ⺫⽬目录就是保存它们的绝好位置。
• ⽤用户采集图⽚片时我们⽣生成了⼀一个唯⼀一的 image
key,我们可以使⽤用这个 image key 来给⽂文件系统
的⽂文件命名。
57. 图⽚片路径:imagePathForKey:
• 在 BNRImageStore.h 中,添加⼀一个新的⽅方法声明
• 并在 BNRImageStore.m 中实现,可以使⽤用给定的
key 在 documents ⺫⽬目录⽣生成⼀一个路径
- (NSString *)imagePathForKey:(NSString *)key
{
NSArray *documentDirectories =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
return [documentDirectory stringByAppendingPathComponent:key];
}
58. NSData
• 为了保存和加载,我们需要把图⽚片的 JPEG 形式拷
⻉贝到内存中的 buffer 。
• NSData 就可以⽅方便的⽣生成,维护和销毁这些类型
的 buffer。
• ⼀一个 NSData 实例持有⼀一定数量字节数的⼆二进制数
据。
• 我们可以使⽤用 NSData 存储图⽚片数据。
59. UIImageJPEGPresentation
• 在 BNRImageStore.m 中修改 setImage:forKey: ⽅方法
来获得⼀一个路径,并且保存图⽚片
- (void)setImage:(UIImage *)i forKey:(NSString *)s
{
[dictionary setObject:i forKey:s];
NSString *imagePath = [self imagePathForKey:s];
NSData *d = UIImageJPEGRepresentation(i, 0.5);
[d writeToFile:imagePath atomically:YES];
}
• UIImageJPEGPresentation 函数的第⼀一个参数是⼀一
个 UIImage 对象,第⼆二个参数是压缩质量,是⼀一个
从 0 -1 的 float,1 是最⾼高质量。
• 返回的是 NSData 实例
60. writeToFile:automatically:
NSData *d = UIImageJPEGRepresentation(i, 0.5);
[d writeToFile:imagePath atomically:YES];
}
• 给 NSData 实例发送 writeToFile:automatically: 消
息,NSData 就被写⼊入⽂文件系统。
61. • automatically 是⼀一个 Boolean 值。
• 如果是 YES, ⽂文件会先被写到⽂文件系统的⼀一个临时
位置,⼀一些写⼊入操作完成,这个⽂文件再被使⽤用它
的 path 重命名,已经存在的⽂文件会被替掉。
• 设成 automatically 可以防⽌止写⼊入过程中应⽤用崩掉
引起的数据损坏
62. 同步删除
• 增加完⽂文件以后,再看⼀一下⽂文件的删除。
• 在 BNRImageStore.m 中,我们要确保把⽂文件从
store 中删除的同时,也把它从⽂文件系统中删除。
- (void)deleteImageForkey:(NSString *)s
{
if (!s) {
return;
}
[dictionary removeObjectForKey:s];
// 确保⽂文件被从 store 删除的同时,也从⽂文件系统删除
NSString *path = [self imagePathForKey:s];
[[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
}
64. imageForKey:
// 如果还没有⽂文件的话,从⽂文件系统加载
- (UIImage *)imageForKey:(NSString *)s
{
// return [dictionary objectForKey:s];
// 如果可能的话,就从字典中获取
UIImage *result = [dictionary objectForKey:s];
if (!result) {
// 从⽂文件创建⼀一个 UIImage
result = [UIImage imageWithContentsOfFile:[self imagePathForKey:s]];
// 如果在⽂文件系统找到⼀一个 image,把它放到 cache 中
if (result) {
[dictionary setObject:result forKey:s];
} else {
NSLog(@"错误:⽆无法找到 %@", [self imagePathForKey:s]);
}
}
return result;
}
65. • 前⾯面对 image 的增加,删除和记载都完成了,还
有⼀一个问题是 BNRItem 被从 store 中删除时,⽂文件
也应该被从⽂文件系统删除。
• 在 BNRItemStore.m 中,导⼊入 BNRImageStore 的头
⽂文件,并且添加下列代码到 removeItem:
- (void)removeItem:(BNRItem *)p
{
// BNRItem 被从 store 中移除的时候,它的 image 也应该被从⽂文件系统删除
NSString *key = [p imageKey];
[[BNRImageStore sharedStore] deleteImageForkey:key];
[allItems removeObjectIdenticalTo:p];
}