在iOS開發過程中,不管是做什么應用,都會碰到數據保存的問題。本地存儲對提高數據交互效率有著重大的意義。本文總結一下數據存儲的幾種方式
在這之前我們需要先對應用程序的文件目錄有所了解,請看下圖
應用程序的文件目錄
Document :保存應用運行時生成的需要持久化的數據,iTunes同步設備時會備份該目錄。例如,游戲應用可將游戲存檔保存在該目錄
Library/Caches :保存應用運行時生成的需要持久化的數據,iTunes同步設備時不會備份該目錄。一般存儲體積大、不需要備份的非重要數據
Library/Preference: 保存應用的所有偏好設置,iOS的Settings(設置)應用會在該目錄中查找應用的設置信息。iTunes同步設備時會備份該目錄
temp :保存應用運行時所需的臨時數據,使用完畢后再將相應的文件從該目錄刪除。應用沒有運行時,系統也可能會清除該目錄下的文件。iTunes同步設備時不會備份該目錄
總結:
為了避免同步過程時間過長,你需要對應用中使用的文件放在哪里做出選擇。很大的數據文件,盡量放置在Caches目錄下,而不是Documents目錄下,Documents目錄下文件將做備份,這樣會很耗時。
存儲方式介紹
- NSKeyedArchiver: 采用歸檔的形式來保存數據沙盒中;
- NSUserDefaults:偏好設置數據存到沙盒的Library/Preferences目錄(本質是plist);
- Write寫入方式: 永久保存在磁盤中;
- SQLite :采用SQLite數據庫來存儲數據。
1.NSKeyedArchiver:(歸檔)
采用歸檔的形式來保存數據,該數據對象需要遵守NSCoding協議,并且該對象對應的類必須提供encodeWithCoder:和initWithCoder:方法。
前一個方法告訴系統怎么對對象進行編碼,而后一個方法則是告訴系統怎么對對象進行解碼。
缺點:
歸檔的形式來保存數據,只能一次性歸檔保存以及一次性解壓。所以只能針對小量數據,而且對數據操作比較笨拙,即如果想改動數據的某一小部分,還是需要解壓整個數據或者歸檔整個數據。
例如對Person對象歸檔保存。
定義Person:
@interface Person:NSObject{//遵守NSCoding協議
NSString *name;//待歸檔類型
}
@implementation Person
-(void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:name forKey:@"name"];
}
-(void)initWithCoder:(NSCoder *)aDecoder
{
name=[aDeCoder decodeObjectforKey:@"name"];
}
歸檔操作:
如果對Person對象name屬性歸檔保存,只需要NSCoder子類NSKeyedArchiver的方法archiveRootObject:toFile: 即可。
// 創建person對象
Person *person = [[Person alloc] init];
person.name = @"DNS";
// 獲取cache
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
// 拼接文件全路徑
NSString *filePath = [cachePath stringByAppendingPathComponent:@"person.data"];
// 把自定義對象歸檔
[NSKeyedArchiver archiveRootObject:person toFile:filePath];
解檔操作:
同樣調用NSCoder子類NSKeyedArchiver的方法unarchiveRootObject:toFile: 即可
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [cachePath stringByAppendingPathComponent:@"person.data"];
// 解檔
[NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
注: initWithCoder什么時候需要調用[super initWithCoder:]
? initWithCoder原理:只要解析文件就會調用,xib,storyboard都是文件,因此只要解析這兩個文件,就會調用initWithCoder。
? 因此如果在storyboard使用自定義view,重寫initWithCoder方法,一定要調用[super initWithCoder:],因為只有系統才知道怎么解析storyboard,如果沒有調用,就解析不了這個文件。
2.NSUserDefaults:(偏好設置,本質是plist)
用來保存應用程序設置和屬性、用戶保存的數據。用戶再次打開程序或開機后這些數據仍然存在。
NSUserDefaults可以存儲的數據類型包括:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要存儲其他類型,則需要轉換為前面的類型,才能用NSUserDefaults存儲。
好處:1.不需要關心文件名
2.快速做鍵值對存儲壞處: 能及時存儲,需要做同步操作,把內存中的數據同步到硬盤上
底層:就是封裝了一個字典
注:在iOS7之前,默認不會馬上跟硬盤同步
存儲:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:@"DNS" forKey:@"name"];
// 在iOS7之前,默認不會馬上把跟硬盤同步
// 同步操作
[userDefaults synchronize];
讀取:
NSString *name = [[NSUserDefaults standardUserDefaults] objectForKey:@"name"];
NSLog(@"%@",name);
3. Write寫入方式:永久保存在磁盤中。
具體方法為:
第一步:獲得文件即將保存的路徑:
//使用C函數NSSearchPathForDirectoriesInDomains來獲得沙盒中目錄的全路徑。
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
//該函數有三個參數,目錄類型、he domain mask、布爾值。
//其中布爾值表示是否需要通過~擴展路徑。而且第一個參數是不變的,即為NSSearchPathDirectory 。
//在IOS中后兩個參數也是不變的,即為:NSUserDomainMask 和 YES。
NSString *ourDocumentPath =[documentPaths objectAtIndex:0];
//還有一種方法是使用NSHomeDirectory函數獲得sandbox的路徑。
NSString *sandboxPath = NSHomeDirectory();
//將Documents添加到sandbox路徑上
NSString *documentPath = [sandboxPath stringByAppendingPathComponent:@"Documents"];
//這兩者的區別就是:使用NSSearchPathForDirectoriesInDomains比在NSHomeDirectory后面添加Document更加安全。因為該文件目錄可能在未來發送的系統上發生改變。
第二步:生成在該路徑下的文件:
//fileName就是保存文件的文件名
NSString *FileName=[documentDirectory stringByAppendingPathComponent:fileName];
第三步:往文件中寫入數據:
//將NSData類型對象data寫入文件,文件名為FileName
[data writeToFile:FileName atomically:YES];
//最后從文件中讀出數據:
NSData data=[NSData dataWithContentsOfFile:FileName options:0 error:NULL];
4. SQLite:采用數據庫來存儲數據。
SQLite (http://www.sqlite.org/docs.html) 是一個輕量級的關系數據庫。iOS SDK很早就支持了SQLite,在使用時,只需要加入 libsqlite3.dylib 依賴以及引入 sqlite3.h 頭文件即可。但是,原生的SQLite API在使用上相當不友好,在使用時,非常不便。于是,開源社區中就出現了一系列將SQLite API進行封裝的庫,而FMDB (https://github.com/ccgus/fmdb) 則是開源社區中的優秀者。
什么是FMDB
FMDB是iOS平臺的SQLite數據庫框架
FMDB以OC的方式封裝了SQLite的C語言APIFMDB的優點
使用起來更加面向對象,省去了很多麻煩、冗余的C語言代碼
對比蘋果自帶的Core Data框架,更加輕量級和靈活
提供了多線程安全的數據庫操作方法,有效地防止數據混亂FMDB的github地址
https://github.com/ccgus/fmdb
二、核心類
FMDB有三個主要的類
FMDatabase
一個FMDatabase對象就代表一個單獨的SQLite數據庫
用來執行SQL語句FMResultSet
使用FMDatabase執行查詢后的結果集FMDatabaseQueue
用于在多線程中執行多個查詢或更新,它是線程安全的
三、打開數據庫
通過指定SQLite數據庫文件路徑來創建FMDatabase對象
//獲得數據庫文件的路徑 NSString *doc=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *fileName=[doc stringByAppendingPathComponent:@"student.sqlite"];
FMDatabase *db = [FMDatabase databaseWithPath:path];
if (![db open]) {
NSLog(@"數據庫打開失敗!");
}
文件路徑有三種情況
具體文件路徑
如果不存在會自動創建空字符串@""
會在臨時目錄創建一個空的數據庫
當FMDatabase連接關閉時,數據庫文件也被刪除nil
會創建一個內存中臨時數據庫,當FMDatabase連接關閉時,數據庫會被銷毀
四、執行更新
在FMDB中,除查詢以外的所有操作,都稱為“更新”
create、drop、insert、update、delete等
使用executeUpdate:方法執行更新
- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments
示例
[db executeUpdate:@"UPDATE t_student SET age = ? WHERE name = ?;", @20, @"Jack"]
五、執行查詢
查詢方法
- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
示例
// 查詢數據
FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student"];
// 遍歷結果集
while ([rs next]) {
NSString *name = [rs stringForColumn:@"name"];
int age = [rs intForColumn:@"age"];
double score = [rs doubleForColumn:@"score"];
}
使用FMDatabaseQueue
線程安全:在多個線程中同時使用一個FMDatabase實例是不明智的。現在你可以為每個線程創建一個FMDatabase對象。 不要讓多個線程分享同一個實例,它無法在多個線程中同時使用。 若此,程序會時不時崩潰,或者報告異常,讓人崩潰。所以,不要初始化FMDatabase對象,然后在多個線程中使用。
請使用 FMDatabaseQueue。以下是使用方法:
//首先創建隊列。
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
//這樣使用。
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
FMResultSet *rs = [db executeQuery:@"select * from foo"];
while([rs next]) { … }}];
//像這樣,輕松地把簡單任務包裝到事務里:
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
if (whoopsSomethingWrongHappened) { *rollback = YES; return; }
// etc… [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]]; }];
FMDatabaseQueue 后臺會建立系列化的G-C-D隊列,并執行你傳給G-C-D隊列的塊。這意味著 你從多線程同時調用調用方法,GDC也會按它接收的塊的順序來執行。