IOS開發之數據持久化

概論
所謂的持久化,就是將數據保存到硬盤中,使得在應用程序或機器重啟后可以繼續訪問之前保存的數據。在iOS開發中,有很多數據持久化的方案,接下來我將嘗試著介紹一下5種方案:
plist文件(屬性列表)

preference(偏好設置)

NSKeyedArchiver(歸檔)

SQLite 3

CoreData

沙盒
在介紹各種存儲方法之前,有必要說明以下沙盒機制。iOS程序默認情況下只能訪問程序自己的目錄,這個目錄被稱為“沙盒”。
1.結構
既然沙盒就是一個文件夾,那就看看里面有什么吧。沙盒的目錄結構如下:

"應用程序包"
Documents
Library
    Caches
    Preferences
tmp

2.目錄特性
雖然沙盒中有這么多文件夾,但是沒有文件夾都不盡相同,都有各自的特性。所以在選擇存放目錄時,一定要認真選擇適合的目錄。
"應用程序包": 這里面存放的是應用程序的源文件,包括資源文件和可執行文件。

NSString *path = [[NSBundle mainBundle] bundlePath];
NSLog(@"%@", path);

Documents: 最常用的目錄,iTunes同步該應用時會同步此文件夾中的內容,適合存儲重要數據。

NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSLog(@"%@", path);

Library/Caches: iTunes不會同步此文件夾,適合存儲體積大,不需要備份的非重要數據。

NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSLog(@"%@", path);

Library/Preferences: iTunes同步該應用時會同步此文件夾中的內容,通常保存應用的設置信息。
tmp: iTunes不會同步此文件夾,系統可能在應用沒運行時就刪除該目錄下的文件,所以此目錄適合保存應用中的一些臨時文件,用完就刪除。

NSString *path = NSTemporaryDirectory();
NSLog(@"%@", path);

plist文件
plist文件是將某些特定的類,通過XML文件的方式保存在目錄中。
可以被序列化的類型只有如下幾種:

NSArray;

NSMutableArray;

NSDictionary;

NSMutableDictionary;

NSData;

NSMutableData;

NSString;

NSMutableString;

NSNumber;

NSDate;

1.獲得文件路徑

NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];

2.存儲

NSArray *array = @[@"123", @"456", @"789"];
[array writeToFile:fileName atomically:YES];

3.讀取

NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);

4.注意
只有以上列出的類型才能使用plist文件存儲。

存儲時使用writeToFile: atomically:方法。 其中atomically表示是否需要先寫入一個輔助文件,再把輔助文件拷貝到目標文件地址。這是更安全的寫入文件方法,一般都寫YES。

讀取時使用arrayWithContentsOfFile:方法。

Preference
1.使用方法

//1.獲得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向文件中寫入內容
[userDefaults setObject:@"AAA"forKey:@"a"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];
//3.讀取文件
NSString *name = [userDefaults objectForKey:@"a"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
NSLog(@"%@, %d, %ld", name, sex, age);

2.注意
偏好設置是專門用來保存應用程序的配置信息的,一般不要在偏好設置中保存其他數據。

如果沒有調用synchronize方法,系統會根據I/O情況不定時刻地保存到文件中。所以如果需要立即寫入文件的就必須調用synchronize方法。

偏好設置會將所有數據保存到同一個文件中。即preference目錄下的一個以此應用包名來命名的plist文件。

NSKeyedArchiver
歸檔在iOS中是另一種形式的序列化,只要遵循了NSCoding協議的對象都可以通過它實現序列化。由于決大多數支持存儲數據的Foundation和Cocoa Touch類都遵循了NSCoding協議,因此,對于大多數類來說,歸檔相對而言還是比較容易實現的。
1.遵循NSCoding協議
NSCoding協議聲明了兩個方法,這兩個方法都是必須實現的。一個用來說明如何將對象編碼到歸檔中,另一個說明如何進行解檔來獲取一個新對象。
遵循協議和設置屬性

//1.遵循NSCoding協議 
@interface Person : NSObject   
//2.設置屬性
@property (strong, nonatomic) UIImage *avatar;
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age;
@end

實現協議方法

//解檔
- (id)initWithCoder:(NSCoder *)aDecoder {
if ([super init]) { 
self.avatar = [aDecoder decodeObjectForKey:@
"avatar"];       
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];     }
return self;
}
//歸檔
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.avatar forKey:@"avatar"];
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}

特別注意

如果需要歸檔的類是某個自定義類的子類時,就需要在歸檔和解檔之前先實現父類的歸檔和解檔方法。即 [super encodeWithCoder:aCoder] 和 [super initWithCoder:aDecoder] 方法;
2.使用
需要把對象歸檔是調用NSKeyedArchiver的工廠方法 archiveRootObject: toFile: 方法。

NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@
"person.data"
];

  
Person *person = [[Person alloc] init];

  
person.avatar = self.avatarView.image;

  
person.name = self.nameField.text;

  
person.age = [self.ageField.text integerValue];

  
[NSKeyedArchiver archiveRootObject:person toFile:file];

需要從文件中解檔對象就調用NSKeyedUnarchiver的一個工廠方法 unarchiveObjectWithFile: 即可。

NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@
"person.data"
];

  
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];

  
if
 (person) {

     
self.avatarView.image = person.avatar;

     
self.nameField.text = person.name;

     
self.ageField.text = [NSString stringWithFormat:@
"%ld"
, person.age];

  
}

3.注意
必須遵循并實現NSCoding協議

保存文件的擴展名可以任意指定

繼承時必須先調用父類的歸檔解檔方法

SQLite3
之前的所有存儲方法,都是覆蓋存儲。如果想要增加一條數據就必須把整個文件讀出來,然后修改數據后再把整個內容覆蓋寫入文件。所以它們都不適合存儲大量的內容。
1.字段類型
表面上SQLite將數據分為以下幾種類型:

integer : 整數

real : 實數(浮點數)

text : 文本字符串

blob : 二進制數據,比如文件,圖片之類的

實際上SQLite是無類型的。即不管你在創表時指定的字段類型是什么,存儲是依然可以存儲任意類型的數據。而且在創表時也可以不指定字段類型。SQLite之所以什么類型就是為了良好的編程規范和方便開發人員交流,所以平時在使用時最好設置正確的字段類型!主鍵必須設置成integer
2. 準備工作
準備工作就是導入依賴庫啦,在iOS中要使用SQLite3,需要添加庫文件:libsqlite3.dylib并導入主頭文件,這是一個C語言的庫,所以直接使用SQLite3還是比較麻煩的。
3.使用
創建數據庫并打開

操作數據庫之前必須先指定數據庫文件和要操作的表,所以使用SQLite3,首先要打開數據庫文件,然后指定或創建一張表。

/**
*  打開數據庫并創建一個表
*/
- (void)openDatabase {
//1.設置文件名
NSString *filename = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@
"person.db"
];
//2.打開數據庫文件,如果沒有會自動創建一個文件
NSInteger result = sqlite3_open(filename.UTF8String, &_sqlite3);
if(result == SQLITE_OK) {
NSLog(@
"打開數據庫成功!"
);   
//3.創建一個數據庫表
char *errmsg = NULL;
sqlite3_exec(_sqlite3, 
"CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)"
, NULL, NULL, &errmsg);
if(errmsg) { 
NSLog(@
"錯誤:%s"
, errmsg);} 
else{     
NSLog(@
"創表成功!");   }
} else{
NSLog(@
"打開數據庫失敗!"
);}
}

執行指令

使用 sqlite3_exec() 方法可以執行任何SQL語句,比如創表、更新、插入和刪除操作。但是一般不用它執行查詢語句,因為它不會返回查詢到的數據。

/**

*  往表中插入1000條數據

*/

- (void)insertData {

NSString *nameStr;

NSInteger age;

for
 (NSInteger i = 0; i < 1000; i++) {

  
nameStr = [NSString stringWithFormat:@
"Bourne-%d"
, arc4random_uniform(10000)];

  
age = arc4random_uniform(80) + 20;

  
NSString *sql = [NSString stringWithFormat:@
"INSERT INTO t_person (name, age) VALUES('%@', '%ld')"
, nameStr, age];

  
char *errmsg = NULL;

  
sqlite3_exec(_sqlite3, sql.UTF8String, NULL, NULL, &errmsg);

  
if
 (errmsg) {

      
NSLog(@
"錯誤:%s"
, errmsg);

  
}

}

NSLog(@
"插入完畢!"
);

}

查詢指令

前面說過一般不使用 sqlite3_exec() 方法查詢數據。因為查詢數據必須要獲得查詢結果,所以查詢相對比較麻煩。示例代碼如下:
sqlite3_prepare_v2() : 檢查sql的合法性

sqlite3_step() : 逐行獲取查詢結果,不斷重復,直到最后一條記錄

sqlite3_coloum_xxx() : 獲取對應類型的內容,iCol對應的就是SQL語句中字段的順序,從0開始。根據實際查詢字段的屬性,使用sqlite3_column_xxx取得對應的內容即可。

sqlite3_finalize() : 釋放stmt

/**

*  從表中讀取數據到數組中

*/

- (void)readData {

   
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1000];

   
char *sql = 
"select name, age from t_person;"
;

   
sqlite3_stmt *stmt;

   
NSInteger result = sqlite3_prepare_v2(_sqlite3, sql, -1, &stmt, NULL);

   
if
 (result == SQLITE_OK) {

       
while
 (sqlite3_step(stmt) == SQLITE_ROW) {

           
char *name = (char *)sqlite3_column_text(stmt, 0);

           
NSInteger age = sqlite3_column_int(stmt, 1);

           
//創建對象

           
Person *person = [Person personWithName:[NSString stringWithUTF8String:name] Age:age];

           
[mArray addObject:person];

       
}

       
self.dataList = mArray;

   
}

   
sqlite3_finalize(stmt);

}

4.總結
總得來說,SQLite3的使用還是比較麻煩的,因為都是些c語言的函數,理解起來有些困難。不過在一般開發過程中,使用的都是第三方開源庫 FMDB,封裝了這些基本的c語言方法,使得我們在使用時更加容易理解,提高開發效率。
FMDB
1.簡介
FMDB是iOS平臺的SQLite數據庫框架,它是以OC的方式封裝了SQLite的C語言API,它相對于cocoa自帶的C語言框架有如下的優點:
使用起來更加面向對象,省去了很多麻煩、冗余的C語言代碼
對比蘋果自帶的Core Data框架,更加輕量級和靈活
提供了多線程安全的數據庫操作方法,有效地防止數據混亂
注:FMDB的gitHub地址
2.核心類
FMDB有三個主要的類:
FMDatabase

一個FMDatabase對象就代表一個單獨的SQLite數據庫,用來執行SQL語句
FMResultSet

使用FMDatabase執行查詢后的結果集
FMDatabaseQueue

用于在多線程中執行多個查詢或更新,它是線程安全的
3.打開數據庫
和c語言框架一樣,FMDB通過指定SQLite數據庫文件路徑來創建FMDatabase對象,但FMDB更加容易理解,使用起來更容易,使用之前一樣需要導入sqlite3.dylib。打開數據庫方法如下:

NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@
"person.db"
];

FMDatabase *database = [FMDatabase databaseWithPath:path];    

if
 (![database open]) {

    
NSLog(@
"數據庫打開失敗!"
);

}

值得注意的是,Path的值可以傳入以下三種情況:
具體文件路徑,如果不存在會自動創建

空字符串@"",會在臨時目錄創建一個空的數據庫,當FMDatabase連接關閉時,數據庫文件也被刪除

nil,會創建一個內存中臨時數據庫,當FMDatabase連接關閉時,數據庫會被銷毀

4.更新
在FMDB中,除查詢以外的所有操作,都稱為“更新”, 如:create、drop、insert、update、delete等操作,使用executeUpdate:方法執行更新:

//常用方法有以下3種:   

- (BOOL)executeUpdate:(NSString*)sql, ...

- (BOOL)executeUpdateWithFormat:(NSString*)format, ...

- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments

//示例

[database executeUpdate:@
"CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)"
];   

//或者  

[database executeUpdate:@
"INSERT INTO t_person(name, age) VALUES(?, ?)"
, @
"Bourne"
, [NSNumber numberWithInt:42]];

5.查詢
查詢方法也有3種,使用起來相當簡單:

- (FMResultSet *)executeQuery:(NSString*)sql, ...

- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...

- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments

查詢示例:

//1.執行查詢

FMResultSet *result = [database executeQuery:@
"SELECT * FROM t_person"
];

//2.遍歷結果集

while
 ([result next]) {

    
NSString *name = [result stringForColumn:@
"name"
];

    
int age = [result intForColumn:@
"age"
];

}

6.線程安全
在多個線程中同時使用一個FMDatabase實例是不明智的。不要讓多個線程分享同一個FMDatabase實例,它無法在多個線程中同時使用。 如果在多個線程中同時使用一個FMDatabase實例,會造成數據混亂等問題。所以,請使用 FMDatabaseQueue,它是線程安全的。以下是使用方法:
創建隊列。

FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];

使用隊列

[queue inDatabase:^(FMDatabase *database) {    

          
[database executeUpdate:@
"INSERT INTO t_person(name, age) VALUES (?, ?)"
, @
"Bourne_1"
, [NSNumber numberWithInt:1]];    

          
[database executeUpdate:@
"INSERT INTO t_person(name, age) VALUES (?, ?)"
, @
"Bourne_2"
, [NSNumber numberWithInt:2]];    

          
[database executeUpdate:@
"INSERT INTO t_person(name, age) VALUES (?, ?)"
, @
"Bourne_3"
, [NSNumber numberWithInt:3]];      

          
FMResultSet *result = [database executeQuery:@
"select * from t_person"
];    

         
while
([result next]) {   

         
}    

}];

而且可以輕松地把簡單任務包裝到事務里:

[queue inTransaction:^(FMDatabase *database, BOOL *rollback) {    

          
[database executeUpdate:@
"INSERT INTO t_person(name, age) VALUES (?, ?)"
, @
"Bourne_1"
, [NSNumber numberWithInt:1]];    

          
[database executeUpdate:@
"INSERT INTO t_person(name, age) VALUES (?, ?)"
, @
"Bourne_2"
, [NSNumber numberWithInt:2]];    

          
[database executeUpdate:@
"INSERT INTO t_person(name, age) VALUES (?, ?)"
, @
"Bourne_3"
, [NSNumber numberWithInt:3]];      

          
FMResultSet *result = [database executeQuery:@
"select * from t_person"
];    

             
while
([result next]) {   

             
}   

           
//回滾

           
*rollback = YES;  

    
}];

FMDatabaseQueue 后臺會建立系列化的G-C-D隊列,并執行你傳給G-C-D隊列的塊。這意味著 你從多線程同時調用調用方法,GDC也會按它接收的塊的順序來執行。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容