对于大多数iOS应用,能够将其功能总结为:提供一套界面,帮助用户管理特定的数据。在这一过程当中,不一样类型的对象要各司其职:模型对象负责保存数据,视图对象负责显示数据,控制器对象负责在模型对象与视图对象之间同步数据。所以,当某个应用要保存和读取数据时,一般要完成的任务是保存和读取相应的模型对象。数组
对 JXHmoepwner 应用,用户能够管理的模型对象是 JXItem 对象。目前 JXHomepwner 不嗯给你保存 JXItem 对象,因此,当用户从新运行 JXHomepwner 时,以前建立的 JXItem 对象都会消失。缓存
固化是由iOS SDK 提供的一种保存和读取对象的机制,使用很是普遍。当应用固化某个对象时,会将该对象的全部属性存入指定文件。当应用解固( unarchive )某个对象时,会从指定的文件读取相应的数据,而后根据数据还原对象。app
为了可以固化或者解固某个对象,相应对象的类必须遵照 NSCoding 协议,而且实现两个必须的方法: encodeWithCoder: 和 initWithCoder: ,代码以下:dom
#import <Foundation/Foundation.h> @interface JXItem : NSObject<NSCoding> /** 建立日期 */ @property (nonatomic,strong,readonly) NSDate * createDate; /** 名称 */ @property (nonatomic,strong) NSString * itemName; /** 编号 */ @property (nonatomic,strong) NSString * serialnumber; /** 价值 */ @property (nonatomic,assign) NSInteger valueInDollars; /** JXImageStore中的键 */ @property (nonatomic,strong) NSString * itemKey; + (instancetype)randomItem; /** * JXItem类指定的初始化方法 * @return 类对象 */ - (instancetype)initWithItemName:(NSString *)name valueInDollars:(NSInteger)value serialNumber:(NSString *)sNumber; - (instancetype)initWithItemName:(NSString *)name; @end
下面为 JXItem 实现 NSCoding 协议的两个必须方法。先实现 encodeWithCoder:(NSCoder *)aCoder 方法。他有一个类型为 NSCoder 的参数,JXItem 的 encodeWithCoder:(NSCoder *)aCoder 方法要将全部的属性都编码至该参数。在固化过程当中,NSCoder 会将 JXItem 转换为键值对形式的数据并写入指定的文件。函数
#import "JXItem.h" @implementation JXItem + (instancetype)randomItem { // 建立不可变数组对象,包含三个形容词 NSArray * randomAdjectiveList = @[ @"Fluffy", @"Rusty", @"Shiny" ]; // 建立不可变数组对象,包含三个名词 NSArray * randomNounList = @[ @"Bear", @"Spork", @"Mac" ]; // 根据数组对象所含的对象的个数,获得随机索引 // 注意:运算符%是模运算符,运算后获得的是余数 NSInteger adjectiveIndex = arc4random() % randomAdjectiveList.count; NSInteger nounIndex = arc4random() % randomNounList.count; // 注意,类型为NSInteger 的变量不是对象 NSString * randomName = [NSString stringWithFormat:@"%@ %@",randomAdjectiveList[adjectiveIndex],randomNounList[nounIndex]]; NSInteger randomValue = arc4random_uniform(100); NSString * randomSerialNumber = [NSString stringWithFormat:@"%c%c%c%c", '0' + arc4random_uniform(10), 'A' + arc4random_uniform(26), '0' + arc4random_uniform(10), 'A' + arc4random_uniform(26)]; JXItem * newItem = [[self alloc] initWithItemName:randomName valueInDollars:randomValue serialNumber:randomSerialNumber]; return newItem; } - (NSString *)description { NSString * descriptionString = [NSString stringWithFormat:@"%@ (%@):Worth $%zd, recorded on %@",self.itemName,self.serialnumber,self.valueInDollars,self.createDate]; return descriptionString; } - (instancetype)initWithItemName:(NSString *)name valueInDollars:(NSInteger)value serialNumber:(NSString *)sNumber { // 调用父类的指定初始化方法 self = [super init]; // 父类的指定初始化方法是否成功建立了对象 if (self) { // 为实例变量设置初始值 _itemName = name; _valueInDollars = value; _serialnumber = sNumber; // 设置_createDate为当前时间 _createDate = [NSDate date]; // 建立一个 NSUUID 对象 NSUUID * uuid = [[NSUUID alloc] init]; NSString * key = [uuid UUIDString]; _itemKey = key; } // 返回初始化后的对象的新地址 return self; } - (instancetype)initWithItemName:(NSString *)name { return [self initWithItemName:name valueInDollars:0 serialNumber:@""]; } - (instancetype)init { return [self initWithItemName:@"Item"]; } - (void)dealloc { NSLog(@"Destoryed:%@",self); } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.itemName forKey:@"itemName"]; [aCoder encodeObject:self.serialnumber forKey:@"serialnumber"]; [aCoder encodeObject:self.createDate forKey:@"createDate"]; [aCoder encodeObject:self.itemKey forKey:@"itemKey"]; [aCoder encodeInteger:self.valueInDollars forKey:@"valueInDollars"];
} @end
在这段代码中,凡是指向对象的指针都会用 encodeObject: forKey: 编码,而 self.valueInDollars 是用 encodeInteger: forKey: 进行编码的。ui
为了可以编码 JXItem 对象,JXItem 的全部属性也必须遵照 NSCoding 协议。this
编码 JXItem 对象时,须要针对每一个属性指定相应的键。当 JXHomepwner 从文件读取相应的数据并从新建立 JXItem 对象时,会根据键来设置属性。当应用须要根据编码后的数据初始化某个对象时,会向该对象发送 - (instancetype)initWithCoder:(NSCoder *)aDecoder 消息。消息应该还原以前经过 - (void)encodeWithCoder:(NSCoder *)aCoder 编码的全部对象,而后将这些对象赋值给相应的属性。编码
#import "JXItem.h" @implementation JXItem + (instancetype)randomItem { // 建立不可变数组对象,包含三个形容词 NSArray * randomAdjectiveList = @[ @"Fluffy", @"Rusty", @"Shiny" ]; // 建立不可变数组对象,包含三个名词 NSArray * randomNounList = @[ @"Bear", @"Spork", @"Mac" ]; // 根据数组对象所含的对象的个数,获得随机索引 // 注意:运算符%是模运算符,运算后获得的是余数 NSInteger adjectiveIndex = arc4random() % randomAdjectiveList.count; NSInteger nounIndex = arc4random() % randomNounList.count; // 注意,类型为NSInteger 的变量不是对象 NSString * randomName = [NSString stringWithFormat:@"%@ %@",randomAdjectiveList[adjectiveIndex],randomNounList[nounIndex]]; NSInteger randomValue = arc4random_uniform(100); NSString * randomSerialNumber = [NSString stringWithFormat:@"%c%c%c%c", '0' + arc4random_uniform(10), 'A' + arc4random_uniform(26), '0' + arc4random_uniform(10), 'A' + arc4random_uniform(26)]; JXItem * newItem = [[self alloc] initWithItemName:randomName valueInDollars:randomValue serialNumber:randomSerialNumber]; return newItem; } - (NSString *)description { NSString * descriptionString = [NSString stringWithFormat:@"%@ (%@):Worth $%zd, recorded on %@",self.itemName,self.serialnumber,self.valueInDollars,self.createDate]; return descriptionString; } - (instancetype)initWithItemName:(NSString *)name valueInDollars:(NSInteger)value serialNumber:(NSString *)sNumber { // 调用父类的指定初始化方法 self = [super init]; // 父类的指定初始化方法是否成功建立了对象 if (self) { // 为实例变量设置初始值 _itemName = name; _valueInDollars = value; _serialnumber = sNumber; // 设置_createDate为当前时间 _createDate = [NSDate date]; // 建立一个 NSUUID 对象 NSUUID * uuid = [[NSUUID alloc] init]; NSString * key = [uuid UUIDString]; _itemKey = key; } // 返回初始化后的对象的新地址 return self; } - (instancetype)initWithItemName:(NSString *)name { return [self initWithItemName:name valueInDollars:0 serialNumber:@""]; } - (instancetype)init { return [self initWithItemName:@"Item"]; } - (void)dealloc { NSLog(@"Destoryed:%@",self); } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.itemName forKey:@"itemName"]; [aCoder encodeObject:self.serialnumber forKey:@"serialnumber"]; [aCoder encodeObject:self.createDate forKey:@"createDate"]; [aCoder encodeObject:self.itemKey forKey:@"itemKey"]; [aCoder encodeInteger:self.valueInDollars forKey:@"valueInDollars"]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { _itemName = [aDecoder decodeObjectForKey:@"itemName"]; _serialnumber = [aDecoder decodeObjectForKey:@"serialnumber"]; _createDate = [aDecoder decodeObjectForKey:@"createDate"]; _itemKey = [aDecoder decodeObjectForKey:@"itemKey"]; _valueInDollars = [aDecoder decodeIntegerForKey:@"valueInDollars"]; } return self; } @end
- (instancetype)initWithCoder:(NSCoder *)aDecoder 也有一个类型为 NSCoder 的参数,和以前的 - (void)encodeWithCoder:(NSCoder *)aCoder 不一样,该参数的做用是为初始化 JXItem 对象提供数据。这段代码经过向 NSCoder 对象发送 decodeObjectForKey: 从新设置相应的属性。atom
每一个iOS应用都有本身的专属的应用沙盒。应用沙盒就是文件系统中的目录,可是iOS系统会将每一个应用沙盒目录与文件系统的其余备份隔离。应用沙盒包含如下多个目录:spa
应用程序包(application bundle) | 包含应用可执行文件和所全部须要资源文件,例如NIB 文件和图像文件,它是一个只读目录 |
Documents/ | 存放应用运行时生成的而且须要保留的数据。iTunes或iCloud会在同步设备时备份该目录。当设备发生故障的时候,能够从iTunes或iCloud恢复该目录中的文件。例如,JXHomepwner 应用可将用户所拥有的物品信息保存在Documents/中。 |
Library/Caches/ | 存放应用运行时生成的须要保留的数据。与Documents/目录不一样的是,iTunes或iCloud不会在同步设备时备份该目录。不备份缓存数据的主要缘由是相关数据的体积可能很大,从而延长同步设备所需的时间。若是数据源是在别处(礼物:Web服务区),就能够将获得的数据保存在 Library/Caches/ 目录。当用户须要回复设备的时候,相关的应用只须要从数据源再次获取数据便可。 |
Library/Preferences/ | 存放全部的偏好设置(Setting)应用也会在该目录中查找应用的设置信息,使用 NSUserDefault 类,能够经过 Library/Preferences/ 目录中的某个特定文件以键值对的形式保存数据。iTunes或iCloud会在同步设备时备份该目录 |
tmp/ | 存放应用运行时所须要的临时数据。当某个应用尚未运行时,iOS系统可能回清除这个应用的 tmp/ 目录下的文件,可是为了节约用户设备空间,不能依赖这种自动清楚机制,而是当应用再也不须要使用 tmp/ 目录中的文件时,就及时手动删除过这些文件。iTunes或iCloud不会在同步设备时备份 tmp/ 目录。经过 NSTemporarDirctory 函数能够获得应用沙盒中的 tmp/ 目录的全路径。 |
下面为 JXHomepwner 增长保存和读取 JXItem 对象的功能,具体要求是:将全部的 JXItem 对象保存至 Documents 目录中的某个文件,并由 JXItemStore 对象负责该文件的写入与读取。为此, JXItemStore 对象须要获取响应文件的全路径。
#import "JXItemStore.h" #import "JXItem.h" #import "JXImageStore.h" @interface JXItemStore () /** 可变数组,用来操做 JXItem 对象 */ @property (nonatomic,strong) NSMutableArray * privateItems; @end @implementation JXItemStore - (NSString *)itemArchivePath { // 注意第一个参数是 NSDocumentDirectory 而不是 NSDocumentationDirectory NSArray * documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); // 从 documentDirectories 数组获取第一个,也是惟一一个文档目录路径 NSString * documentDirectory = [documentDirectories firstObject]; return [documentDirectory stringByAppendingPathComponent:@"items.archive"]; } // 单粒对象 + (instancetype)sharedStore { static JXItemStore * sharedStore = nil; // 判断是否须要建立一个 sharedStore 对象 if (!sharedStore) { sharedStore = [[self alloc] init]; } return sharedStore; } - (NSArray *)allItem { return [self.privateItems copy]; } - (JXItem *)createItem { JXItem * item = [JXItem randomItem]; [self.privateItems addObject:item]; return item; } /** * 还能够调用 [self.privateItems removeObject:item] * [self.privateItems removeObjectIdenticalTo:item] 与上面的方法的区别就是:上面的方法会枚举数组,向每个数组发送 isEqual: 消息。 * isEqual: 的做用是判断当前对象和传入对象所包含的数据是否相等。可能会复写 这个方法。 * removeObjectIdenticalTo: 方法不会比较对象所包含的数据,只会比较指向对象的指针 * * @param item 须要删除的对象 */ - (void)removeItem:(JXItem *)item { [self.privateItems removeObjectIdenticalTo:item]; [[JXImageStore sharedStore] deleteImageForKey:item.itemKey]; } - (void)moveItemAtIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { // 若是起始位置和最终位置相同,则不懂 if (fromIndex == toIndex) return; // 须要移动的对象的指针 JXItem * item = self.privateItems[fromIndex]; // 将 item 从 allItem 数组中移除 [self.privateItems removeObjectAtIndex:fromIndex]; // 根据新的索引位置,将item 插入到allItem 数组中 [self.privateItems insertObject:item atIndex:toIndex]; } #pragma mark - 懒加载 - (NSMutableArray *)privateItems{ if (_privateItems == nil) { _privateItems = [[NSMutableArray alloc] init]; } return _privateItems; } @end
经过C函数 NSSearchPathForDirectoriesInDomains 能够获得沙盒总的某种目录的全路径。该函数有三个实参,其中后两个实参须要传入固定值。第一个实参是 NSSearchPathDirectory 类型的常量,负责制定目录的类型。例如,传入 NSCachesDirectory 能够获得沙盒中的 Caches 目录的路径。
将某个 NSSearchPathDirectory 常量(例如 NSDocumentDirectory )做为关键词查找文档,就能找到其余的常量。
NSSearchPathForDirectoriesInDomains 函数的返回值是 NSArray 对象,包含的都是 NSString 对象。为何该函数的返回值不是一个 NSString 对象?这是由于对 Mac OS X,可能会有多个目录匹配某组指定的查询条件。可是在iOS上,一种目录类型只会有一个匹配的目录。因此上面的这段代码会获取数组的第一个也是惟一一个 NSString 对象,而后在该字符穿的后面追加固化文件的文件名,并最终获得保存 JXItem 对象的文件路径。
以前咱们修改了 JXItem 类,能使 JXHomepwner 固化 JXItem 。 此外咱们还要解决的两个问题是:1. 如何保存或者读取数据? 2. 什么时候保存或者读取数据? 先解决 保存 问题。应用应该在退出时,经过 NSKeyedArchiver 类保存 JXItem 对象。
#import <Foundation/Foundation.h> @class JXItem; @interface JXItemStore : NSObject /** 存放 JXItem 对象数组 */ @property (nonatomic,readonly) NSArray * allItem; // 注意,这是一个类方法,前缀是+ + (instancetype)sharedStore; - (JXItem *)createItem; /** * 删除对象 * * @param item 须要删除的对象 */ - (void)removeItem:(JXItem *)item; /** * 移动对象 * * @param fromIndex 移动对象的起始位置 * @param toIndex 移动后的位置 */ - (void)moveItemAtIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex; - (BOOL)saveChanges; @end
#import "JXItemStore.h" #import "JXItem.h" #import "JXImageStore.h" @interface JXItemStore () /** 可变数组,用来操做 JXItem 对象 */ @property (nonatomic,strong) NSMutableArray * privateItems; @end @implementation JXItemStore - (BOOL)saveChanges { NSString * path = [self itemArchivePath]; // 若是固化成功就返回YES return [NSKeyedArchiver archiveRootObject:self.privateItems toFile:path]; } - (NSString *)itemArchivePath { // 注意第一个参数是 NSDocumentDirectory 而不是 NSDocumentationDirectory NSArray * documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); // 从 documentDirectories 数组获取第一个,也是惟一一个文档目录路径 NSString * documentDirectory = [documentDirectories firstObject]; return [documentDirectory stringByAppendingPathComponent:@"items.archive"]; } // 单粒对象 + (instancetype)sharedStore { static JXItemStore * sharedStore = nil; // 判断是否须要建立一个 sharedStore 对象 if (!sharedStore) { sharedStore = [[self alloc] init]; } return sharedStore; } - (NSArray *)allItem { return [self.privateItems copy]; } - (JXItem *)createItem { JXItem * item = [JXItem randomItem]; [self.privateItems addObject:item]; return item; } /** * 还能够调用 [self.privateItems removeObject:item] * [self.privateItems removeObjectIdenticalTo:item] 与上面的方法的区别就是:上面的方法会枚举数组,向每个数组发送 isEqual: 消息。 * isEqual: 的做用是判断当前对象和传入对象所包含的数据是否相等。可能会复写 这个方法。 * removeObjectIdenticalTo: 方法不会比较对象所包含的数据,只会比较指向对象的指针 * * @param item 须要删除的对象 */ - (void)removeItem:(JXItem *)item { [self.privateItems removeObjectIdenticalTo:item]; [[JXImageStore sharedStore] deleteImageForKey:item.itemKey]; } - (void)moveItemAtIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { // 若是起始位置和最终位置相同,则不懂 if (fromIndex == toIndex) return; // 须要移动的对象的指针 JXItem * item = self.privateItems[fromIndex]; // 将 item 从 allItem 数组中移除 [self.privateItems removeObjectAtIndex:fromIndex]; // 根据新的索引位置,将item 插入到allItem 数组中 [self.privateItems insertObject:item atIndex:toIndex]; } #pragma mark - 懒加载 - (NSMutableArray *)privateItems{ if (_privateItems == nil) { _privateItems = [[NSMutableArray alloc] init]; } return _privateItems; } @end
这段代码中的 archiveRootObject: toFile: 会将 privateItems 中的全部 JXItem 对象都保存至路径为 itemArchivePath 文件。工做原理:
#import "AppDelegate.h" #import "JXItemsViewController.h" #import "JXItemStore.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; // 添加初始化代码 // 建立 JXItemsViewController 对象 JXItemsViewController * itemsViewController = [[JXItemsViewController alloc] init]; // 将 JXItemsViewController 的标示图加入窗口 self.window.rootViewController = itemsViewController; // 将 UINavigationController 对象设置为 UIWindow 对象的根视图控制器。 // 这样就能够将 UINavigationController 对象的视图添加到屏幕中 UINavigationController * navController = [[UINavigationController alloc] initWithRootViewController:itemsViewController]; self.window.rootViewController = navController; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; } - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } - (void)applicationDidEnterBackground:(UIApplication *)application { BOOL success = [[JXItemStore sharedStore] saveChanges]; if (success) { NSLog(@"Saved"); } else { NSLog(@"Faild"); } } - (void)applicationWillEnterForeground:(UIApplication *)application { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } - (void)applicationDidBecomeActive:(UIApplication *)application { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } - (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } @end
#import "JXItemStore.h" #import "JXItem.h" #import "JXImageStore.h" @interface JXItemStore () /** 可变数组,用来操做 JXItem 对象 */ @property (nonatomic,strong) NSMutableArray * privateItems; @end @implementation JXItemStore - (BOOL)saveChanges { NSString * path = [self itemArchivePath]; // 若是固化成功就返回YES return [NSKeyedArchiver archiveRootObject:self.privateItems toFile:path]; } - (NSString *)itemArchivePath { // 注意第一个参数是 NSDocumentDirectory 而不是 NSDocumentationDirectory NSArray * documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); // 从 documentDirectories 数组获取第一个,也是惟一一个文档目录路径 NSString * documentDirectory = [documentDirectories firstObject]; return [documentDirectory stringByAppendingPathComponent:@"items.archive"]; } // 单粒对象 + (instancetype)sharedStore { static JXItemStore * sharedStore = nil; // 判断是否须要建立一个 sharedStore 对象 if (!sharedStore) { sharedStore = [[self alloc] init]; } return sharedStore; } - (NSArray *)allItem { return [self.privateItems copy]; } - (JXItem *)createItem { JXItem * item = [JXItem randomItem]; JXItem * item = [[JXItem alloc] init]; [self.privateItems addObject:item]; return item; } /** * 还能够调用 [self.privateItems removeObject:item] * [self.privateItems removeObjectIdenticalTo:item] 与上面的方法的区别就是:上面的方法会枚举数组,向每个数组发送 isEqual: 消息。 * isEqual: 的做用是判断当前对象和传入对象所包含的数据是否相等。可能会复写 这个方法。 * removeObjectIdenticalTo: 方法不会比较对象所包含的数据,只会比较指向对象的指针 * * @param item 须要删除的对象 */ - (void)removeItem:(JXItem *)item { [self.privateItems removeObjectIdenticalTo:item]; [[JXImageStore sharedStore] deleteImageForKey:item.itemKey]; } - (void)moveItemAtIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { // 若是起始位置和最终位置相同,则不懂 if (fromIndex == toIndex) return; // 须要移动的对象的指针 JXItem * item = self.privateItems[fromIndex]; // 将 item 从 allItem 数组中移除 [self.privateItems removeObjectAtIndex:fromIndex]; // 根据新的索引位置,将item 插入到allItem 数组中 [self.privateItems insertObject:item atIndex:toIndex]; } #pragma mark - 懒加载 - (NSMutableArray *)privateItems{ NSString * path = [self itemArchivePath]; _privateItems = [NSKeyedUnarchiver unarchiveObjectWithFile:path]; // 若是没有就建立 if (_privateItems == nil) { _privateItems = [[NSMutableArray alloc] init]; } return _privateItems; } @end
这段代码中的 unarchiveObjectWithFile: 类方法会建立一个 NSKeyedUnarchiver 对象,而后根据指定的路径载入固化文件。接着, NSKeyedUnarchiver 类会查看固化文件中的跟对象,而后根据对象的类型建立相应的对象。固化的时候是保存的什么对象,那么解固的时候就会返回什么对象。