14 Saving Loading and Application States

65
保存,加载,及应程序状态 范圣刚,[email protected] www.tfan.org

description

保存,加载和程序状态

Transcript of 14 Saving Loading and Application States

Page 1: 14 Saving Loading and Application States

保存,加载,及应⽤用程序状态

范圣刚,[email protected] www.tfan.org

Page 2: 14 Saving Loading and Application States

•在⼀一个 iOS application 中保存和加载数据有很多⽅方法。

•这⼀一个主题我们来看⼀一下⼀一些 常⻅见的机制,以及在 iOS 上对⽂文件系统进⾏行读写需要理解的概念

Page 3: 14 Saving Loading and Application States

Archiving

Page 4: 14 Saving Loading and Application States

•任何 iOS 应⽤用实际都在做⼀一件事情:提供给⽤用户⼀一个界⾯面让他们来操作数据。

•在应⽤用中的每⼀一个对象在这个过程中都扮演下⾯面的⼀一个⾓角⾊色。

• Model 对象,负责持有⽤用户操作的数据

• View 对象,只是体现这些数据

• Controllers,负责应⽤用运⾏行到底是怎么回事

•因此当我们讨论保存和加载数据时,我们⼏几乎总是在谈论保存和加载 Model 对象

Page 5: 14 Saving Loading and Application States

•在 Homepwner 中,⽤用户操作的模型对象是 BNRItem 的实例。

•如果在应⽤用退出后再启动时 BNRItem 的实例能够持久存在,Homepwner 将真正成为⼀一个有⽤用的应⽤用

•在这⼀一章,我们将使⽤用 archiving 来保存和加载 BNRItem

Page 6: 14 Saving Loading and Application States

• Archiving 是在 iOS 上持久化模型数据的⼀一种 常⻅见的⽅方法。

• Archiving ⼀一个对象会记录它所有的实例变量并且把他们保存到⽂文件系统。

•Unarchiving ⼀一个对象就是从⽂文件系统加载数据,并且再从这个记录⽣生成对象。

Page 7: 14 Saving Loading and Application States

NSCoding

•需要把它的实例进⾏行 archive 和 unarchive 的类必须符合 NSCoding protocol。

•并且实现它的两个必须的⽅方法,encodeWithCoder: 和 initWithCoder:

@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;- (id)initWithCoder:(NSCoder *)aDecoder;

Page 8: 14 Saving Loading and Application States

encode

•在 BNRItem.h 中增加 NSCoding protocol 的声明@interface BNRItem : NSObject <NSCoding>

•然后是实现两个必须的⽅方法,⾸首先是 encodeWithCoder:

Page 9: 14 Saving Loading and Application States

•当 BNRItem 被发送 encodeWithCoder: 消息时,它将把它的所有的实例变量编码到作为⼀一个参数传⼊入的 NSCoder 对象中

•我们可以把 NSCoder 对象想成⼀一个数据容器,负责组织这些数据

•NSCoder 以key-value pair 的⽅方式组织数据

Page 10: 14 Saving Loading and Application States

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"];}

Page 11: 14 Saving Loading and Application States

encodeXXX:forKey

•指向对象的指针使⽤用 encodeObject:forKey: 编码

• valueInDollars 使⽤用 encodeInt:forKey: 编码,还有很可以 encode 的类型

Page 12: 14 Saving Loading and Application States

•不管被编码的值是什么类型,总有⼀一个 key 存在

• key 是⼀一个⽤用来标识哪个实例变量在被编码的字符串。按照惯例,这个 key 就是被编码的实例变量的名字

Page 13: 14 Saving Loading and Application States

编码对象的递归过程•⼀一个对象被编码时,被发送 encodeWithCoder: 。当⼀一个对象被发送 encodeWithCoder:,它⽤用同样的⽅方式编码它的实例变量 - 也是给它们发送 encodeWithCoder:

•这样编码⼀一个对象就是⼀一个对象编码其他对象的递归过程

Page 14: 14 Saving Loading and Application States

编码⼀一个对象

•想要被编码,这些对象必须也符合 NSCoding

•NSDate, NSString 都是 NSCoding 兼容的

Page 15: 14 Saving Loading and Application States

initWithCoder:

• encoding 时使⽤用 key 的⺫⽬目的是为了之后在 BNRItem 被从⽂文件系统加载时⽤用来获取已编码的值

•被从⼀一个 archive 中加载的对象被发送的是 initWithCoder: 消息。

•这个⽅方法攫取所有在 encodeWithCoder: 中被编码的对象,并且把它们分配给合适的实例变量。

•在 BNRItem.m 中实现这个⽅方法

Page 16: 14 Saving Loading and Application States

decodeXXXForKey:

•这个⽅方法也有⼀一个 NSCoder 参数,它⾥里⾯面的数据是供正在被初始化的 BNRItem 使⽤用的

•使⽤用 decodeObjectForKey: 取回对象(object),decodeIntForKey: 取回 valueInDollar

- (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;}

Page 17: 14 Saving Loading and Application States

•经过前⾯面的改造之后,BNRItem 现在是 NSCoding 兼容的了,⽽而且也可以使⽤用 archiving 从⽂文件系统进⾏行保存和加载。

•现在还有两个问题要考虑:•我们需要⼀一种⽅方法来开启保存和加载操作?(现在只是可以 archiving 和 unarchiving)

•另外,我们需要在⽂文件系统找⼀一个地⽅方来存储我们要保存的 BNRItem

Page 18: 14 Saving Loading and Application States

Application Sandbox

Page 19: 14 Saving Loading and Application States

•每⼀一个 iOS 应⽤用都有它⾃自⼰己的 applicaton sandbox(应⽤用沙箱)。

•⼀一个 application sandbox 是在⽂文件系统上的⼀一个和⽂文件系统其余部分隔离的⺫⽬目录。

•应⽤用必须位于这个 sandbox 内,并且没有其他的应⽤用可以访问你的 sandbox。

Page 20: 14 Saving Loading and Application States

Application Sandbox

Page 21: 14 Saving Loading and Application States

application sandbox

• Library/Preferences/

• tmp/

•Documents/

• Library/Caches

Page 22: 14 Saving Loading and Application States

Library/Preferences/

•所有的⾸首选项都存在这⾥里;“Settings”应⽤用也在这⾥里查找应⽤用程序⾸首选项

• Library/Preferences 被 NSUserDefaults 类⾃自动化的进⾏行处理

•当设备和 iTunes 或 iCloud 同步时会被备份

Page 23: 14 Saving Loading and Application States

tmp/

•⽤用于写⼊入应⽤用运⾏行时临时⽤用到的数据,⽤用完以后应该从这个⺫⽬目录删除

•不备份•使⽤用函数 NSTemporaryDirectory 拿到 application

sandbox 中 tmp ⺫⽬目录的路径

Page 24: 14 Saving Loading and Application States

Documents

•需要持久化存储的数据可以写到这⾥里•备份

Page 25: 14 Saving Loading and Application States

Library/Caches

•应⽤用程序⽣生成的,还希望能够持久存在•不会被备份•不被备份的⼀一个主要的原因就是这些⽂文件可能会很⼤大,会延⻓长同步设备的时间

Page 26: 14 Saving Loading and Application States

构造⼀一个⽂文件路径•来⾃自 Homepwner 的 BNRItem 将被保存到

Documents ⺫⽬目录中⼀一个单独的⽂文件中

• BNRItemStore 将处理⽂文件写⼊入和读取的⼯工作

•⾸首先,我们需要来构建这个⽂文件的路径•在 BNRItemStore 中声明和实现⼀一个新的⽅方法

Page 27: 14 Saving Loading and Application States

•NSSearchPathForDirectoriesInDomains 函数搜索⽂文件系统来查找符合给定参数标准的路径

•在 iOS 上, 后两个参数总是⼀一样的

•第⼀一个参数是指定了在 sandbox 中⺫⽬目录的常量。⽐比如搜索 NSCachesDirectory 将会返回在应⽤用的 sandbox 中的 Caches ⺫⽬目录

•返回值是⼀一个字符串数组,在 iOS 上只有⼀一个,所以只取第⼀一个就可以了

- (NSString *)itemArchivePath{ NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentDirectory = [documentDirectories objectAtIndex:0]; return [documentDirectory stringByAppendingPathComponent:@"items.archive"];}

Page 28: 14 Saving Loading and Application States

NSKeyedArchiver 和 NSKeyedUnarchiver

Page 29: 14 Saving Loading and Application States

•现在我们已经有了⼀一个⽂文件系统的路径,同时模型对象(BNRItem)也可以被保存到⽂文件系统了

•我们可以使⽤用 NSKeyedArchiver 在应⽤用 “退出” 时保存 BNRItems

•在 BNRItemStore.h 中声明⼀一个新的⽅方法:- (BOOL)saveChanges;

Page 30: 14 Saving Loading and Application States

archiveRootObject:

•在 BNRItemStore.m 中实现这个⽅方法:发送 archiveRootObject:toFile: 消息到 NSKeyedArchiver 类

• archiveRootObject:toFile: ⽅方法负责保存 allItems 中每个单独的 BNRItem 到 itemArchivePath

- (BOOL)saveChanges{ // 返回成功或失败 NSString *path = [self itemArchivePath]; return [NSKeyedArchiver archiveRootObject:allItems toFile:path];}

Page 31: 14 Saving Loading and Application States

•这个⽅方法会⾸首先创建⼀一个 NSKeyedArchiver 的实例。

•然后发送 encodeWithCoders: 消息到 root object(这⾥里是 allItems )。

•NSKeyedArchiver 是 NSCoder 的⼦子类,所以可以传给 encodeWithCoder:

Page 32: 14 Saving Loading and Application States

• allItems 收到 encodeWithCoder: 消息后,会再发给它包含的所有的对象,传递的同样是 NSKeyedArchiver。

•这个数组的内容,也就是⼀一堆 BNRItems,再 encode 它们的实例变量到同⼀一个 NSKeyedArchiver。

•⼀一旦所有这些对象都被编码之后,NSKeyedArchiver 就把它收集到的这些数据写到它的 path 参数(我们前⾯面构建的路径)

Page 33: 14 Saving Loading and Application States

Archiving

Page 34: 14 Saving Loading and Application States

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 "); }}

Page 35: 14 Saving Loading and Application States

⽂文件写⼊入确认•构建并在模拟器中运⾏行,创建⼀一个新的

BNRItems。然后点击 home 键离开应⽤用,检查控制台,我们应该看到前⾯面代码中记录的⽇日志。

•我们也可以在电脑的⺫⽬目录中确认⼀一下⽂文件是否写⼊入成功。在 Finder 中,Command-Shift-G, 键⼊入“~/Library/Application Support/iPhone Simulator”, 找到应⽤用的 sandbox 查看⼀一下

Page 36: 14 Saving Loading and Application States

•下⼀一步编写加载⽂文件的代码,我们可以在 BNRItemStore 被创建时使⽤用 NSKeyedUnarchiver

Page 37: 14 Saving Loading and Application States

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 到它的实例

Page 38: 14 Saving Loading and Application States

•下⾯面我们来看⼀一下加载的过程。•检测 archive 中 root 对象的类型并且⽣生成这个类型的实例,在这⾥里,这个类型将是⼀一个 NSMutableArray,因为我们就是使⽤用这个类型的 root object 来⽣生成的这个 archive

Page 39: 14 Saving Loading and Application States

•然后新分配的 NSMutableArray 会被发送 initWithCoder: , NSKeyedUnarchiver 被作为参数传⼊入;

•然后就是数组开始从 NSKeyedUnarchiver 解码它的内容( BNRItem 的实例), 给这些对象发送 initWithCoder: 消息,传递同样的 NSKeyedUnarchiver

•再次构建并运⾏行,测试⼀一下加载功能

Page 40: 14 Saving Loading and Application States

不⽣生成随机数据了•因为我们现在已经可以保存和加载 BNRItem 了,所以我们可以把⽣生成随机数据的功能删除掉。

•在 BNRItemStore.m 中修改 createItem 的实现让它⽣生成⼀一个不带随机数据的空的 BNRItem

- (BNRItem *)createItem{// BNRItem *p = [BNRItem randomItem]; BNRItem *p = [[BNRItem alloc] init];

[allItems addObject:p]; return p;}

Page 41: 14 Saving Loading and Application States

应⽤用程序的 States 和 Transitions

Page 42: 14 Saving Loading and Application States

•在 Hompwner 中,我们是在应⽤用程序进⼊入 backgroud state 时 archive BNRItem 的。

•下⾯面我们来看⼀一下应⽤用程序状态有关的内容,包括这些状态是怎么被触发的,以及我们怎么获得通知

•后⾯面是⼀一个有关状态的概要图

Page 43: 14 Saving Loading and Application States

应⽤用状态

Page 44: 14 Saving Loading and Application States

Not running

•不执⾏行任何代码也不占⽤用 RAM 内存

Page 45: 14 Saving Loading and Application States

active state

•⽤用户启动⼀一个应⽤用之后,就进⼊入了 active state 。

•应⽤用界⾯面在屏幕上显⽰示,正在接收事件,并且它的代码在处理这些事件

Page 46: 14 Saving Loading and Application States

inactive state

•在 active state 下,应⽤用可以被系统事件临时性的打断,例如 SMS 消息,推送通知,来电,或者 alarm。应⽤用上⾯面会被叠加⼀一个界⾯面来处理这个事件。这种状态被称为 inactive state 。

Page 47: 14 Saving Loading and Application States

•在 inactive state 下,应⽤用⼤大部分是可⻅见的,并且在执⾏行代码,但是没有在接收事件。应⽤用⼀一般在 inactive state 下会停留很短的⼀一个时间。

•可以通过按下设备顶部的锁定按钮(lock)让活动的应⽤用程序强制进⼊入 inactive state

•直到设备被解锁(unlock)前,应⽤用会⼀一直保持 inactive

Page 48: 14 Saving Loading and Application States

background state

•当⽤用户按下 home 键或是某种其他⽅方式切换到其它应⽤用时,应⽤用进⼊入 background state。

•在 background state 下,界⾯面不可⻅见,也不能接收消息,但是仍然可以执⾏行代码。

•默认情况下,进⼊入 background state 的应⽤用在进⼊入 suspended state 前有 5 秒的时间。

Page 49: 14 Saving Loading and Application States

suspended state

•在 suspended state 下的应⽤用不能执⾏行代码,⽆无法看到它的界⾯面,并且 suspended 时不需要的资源都会被销毁。

Page 50: 14 Saving Loading and Application States

• suspended 的应⽤用处于冻结状态,当⽤用户重新启动它的时候,可以被快速的解冻。

•被销毁的资源都是可以被重新加载的,像缓存的图⽚片,系统管理的缓存,以及其他图⽚片数据。

•我们不需要考虑这些资源的销毁和重新记载,应⽤用程序会⾃自动处理。

Page 51: 14 Saving Loading and Application States

应⽤用状态表

State Visible Receives Events

Executes Code

Not Running No No No

Active Yes Yes Yes

Inactive Mostly No Yes

Background No No Yes

Suspended No No No

Page 52: 14 Saving Loading and Application States

background 时保存数据•应⽤用变更状态时,应⽤用 delegate 会被发送⼀一个消息。像 application:didFinishLaunchingWithOptions: 等。

•我们可以在这些⽅方法中实现代码采取⼀一些适当的操作。

Page 53: 14 Saving Loading and Application States

•我们应该在往 background state 转换时保存关键的修改和应⽤用程序的状态。

•因为这是应⽤用程序在进⼊入 suspended state 前还可以执⾏行代码的最后时刻。

•⼀一旦进⼊入了 suspended state,应⽤用有可能会被操作系统根据⾃自⾝身的需要终⽌止。

Page 54: 14 Saving Loading and Application States

使⽤用 NSData 写⼊入⽂文件系统

Page 55: 14 Saving Loading and Application States

•对 Homepwner 进⾏行 archiving 时我们保存和加载了针对每个 BNRItem 的 imageKey。

•我们可以扩展 image store,在它们被添加时保存图⽚片,然后在需要的时候可以提取出来。

Page 56: 14 Saving Loading and Application States

• BNRItem 实例的 image 是通过⽤用户交互⽣生成的,并且只存储在应⽤用程序内部。

•这样 Documents ⺫⽬目录就是保存它们的绝好位置。

•⽤用户采集图⽚片时我们⽣生成了⼀一个唯⼀一的 image key,我们可以使⽤用这个 image key 来给⽂文件系统的⽂文件命名。

Page 57: 14 Saving Loading and Application States

图⽚片路径: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];}

Page 58: 14 Saving Loading and Application States

NSData

•为了保存和加载,我们需要把图⽚片的 JPEG 形式拷⻉贝到内存中的 buffer 。

•NSData 就可以⽅方便的⽣生成,维护和销毁这些类型的 buffer。

•⼀一个 NSData 实例持有⼀一定数量字节数的⼆二进制数据。

•我们可以使⽤用 NSData 存储图⽚片数据。

Page 59: 14 Saving Loading and Application States

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 的 "oat,1 是 ⾼高质量。

•返回的是 NSData 实例

Page 60: 14 Saving Loading and Application States

•给 NSData 实例发送 writeToFile:automatically: 消息,NSData 就被写⼊入⽂文件系统。

NSData *d = UIImageJPEGRepresentation(i, 0.5); [d writeToFile:imagePath atomically:YES];}

writeToFile:automatically:

Page 61: 14 Saving Loading and Application States

• automatically 是⼀一个 Boolean 值。

•如果是 YES, ⽂文件会先被写到⽂文件系统的⼀一个临时位置,⼀一些写⼊入操作完成,这个⽂文件再被使⽤用它的 path 重命名,已经存在的⽂文件会被替掉。

•设成 automatically 可以防⽌止写⼊入过程中应⽤用崩掉引起的数据损坏

Page 62: 14 Saving Loading and Application States

同步删除•增加完⽂文件以后,再看⼀一下⽂文件的删除。•在 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];}

Page 63: 14 Saving Loading and Application States

imageWithContentsOfFile:

•现在⽂文件已经被存储在⽂文件系统了,BNRImageStore 在被请求的时候需要加载这些⽂文件。

•UIImage 的类⽅方法 imageWithContentsOfFile: 可以从⽂文件中作为⼀一个 image 读⼊入。

•在 BNRImageStore.m 中替换 imageForKey: , 这样BNRImageStore 在它没有这个⽂文件的时候就可以从⽂文件系统进⾏行加载。

Page 64: 14 Saving Loading and Application States

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;}

Page 65: 14 Saving Loading and Application States

•前⾯面对 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];}