對於大多數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 類會查看固化文件中的跟對象,而後根據對象的類型建立相應的對象。固化的時候是保存的什麼對象,那麼解固的時候就會返回什麼對象。