導(dǎo)語
數(shù)據(jù)持久化是一種非易失性存儲技術(shù),在重啟動計算機(jī)或設(shè)備后也不會丟失數(shù)據(jù),是將內(nèi)存中的數(shù)據(jù)模型轉(zhuǎn)換為存儲模型,以及將存儲模型轉(zhuǎn)換為內(nèi)存中的數(shù)據(jù)模型的統(tǒng)稱。數(shù)據(jù)模型可以是任何數(shù)據(jù)結(jié)構(gòu)或?qū)ο竽P?存儲模型可以是關(guān)系模型、XML、二進(jìn)制流等。持久化技術(shù)主要用于MVC模型中的model層。目前iOS平臺上主要使用如下的四種技術(shù):
一.NSUserDefaults(關(guān)鍵詞:屬性列表、xml序列化)
什么是NSUserDefaults?
在介紹NSUserDefaults之前,我們有必要先了解屬性列表的概念:屬性列表是一種基于xml序列化的數(shù)據(jù)永久存儲文件,又稱plist文件,原理是將一些基本數(shù)據(jù)類型讀寫進(jìn)plist文件(注:plist文件是xml格式文件,因為常用于存儲配置信息,所以又稱作plist格式文件)并以明文方式存儲于設(shè)備中。許多OC的基本數(shù)據(jù)類型(如NSArray、NSString等)本身提供了向plist文件讀寫的方法,但實際項目中我們用的更多的是NSUserDefaults,NSUserDefaults是蘋果基于屬性列表所封裝的一個單例類,該類提供了基本數(shù)據(jù)類型的plist文件存儲方法,因其使用方便,代碼易懂,NSUserDefaults成為了最常用的數(shù)據(jù)持久化方式之一。
NSUserDefaults常用方法
//從 NSUserDefaults 中取出 key 值所對應(yīng)的 Value
id = [[NSUserDefaults standardUserDefaults] objectForKey:(NSString *)];
//將數(shù)據(jù)對象存儲到 NSUserDefaults 中
[[NSUserDefaults standardUserDefaults] setObject:(id)
forKey:(NSString *)];
//將數(shù)據(jù)對象從 NSUserDefaults 中移除
[[NSUserDefaults standardUserDefaults] removeObjectForKey(NSString *)];
//同步更新到Plist文件,當(dāng)修改了 NSUserDefaults 的數(shù)據(jù)后,必須進(jìn)行此步操作
[[NSUserDefaults standardUserDefaults] synchronize];
NSUserDefaults特點
- NSUserDefaults常用于存儲OC基本數(shù)據(jù)類型,不適合存儲自定義對象,NSUserDefaults支持的數(shù)據(jù)類型有:NSNumber(NSInteger、float、double),NSString,NSDate,NSArray,NSDictionary,BOOL.
- 自定義對象可以轉(zhuǎn)化成基本類型NSData后再使用NSUserDefaults進(jìn)行存儲,但并不常用。
- 當(dāng)plist文件存儲的數(shù)據(jù)發(fā)生改變(寫操作)時,需要調(diào)用synchronize方法同步,否則數(shù)據(jù)無法同步保存。
- Key值應(yīng)具有唯一性,重名時將覆蓋先前的key值。
- 實際開發(fā)中,NSUserDefaults常用于存儲配置信息,優(yōu)點是簡便,缺點是所有數(shù)據(jù)都以明文存儲在plist文件中,容易被解讀導(dǎo)致安全性不高。
二.對象歸檔(關(guān)鍵詞:序列化)
什么是對象歸檔?
和屬性列表一樣,對象歸檔也是將對象寫入文件并保存在硬盤內(nèi),所以本質(zhì)上是另一種形式的序列化(儲存模型不同)。雖說任何對象都可以被序列化,但只有某些特定的對象才能放置到某個集合類(例如:NSArray、NSMutableArray、NSDictionary、NSData等)中,并使用該集合類的方法在屬性列表存儲中讀寫,一旦將包含了自定義對象的數(shù)組寫入屬性列表,程序就會報錯。歸檔與屬性列表方式不同,屬性列表只有指定的一些對象才能進(jìn)行持久化且明文存儲,而歸檔是任何實現(xiàn)了NSCopying協(xié)議的對象都可以被持久化,且歸檔后的文件是加密的。對象歸檔涉及兩個類:NSKeyedArchiver和NSKeyedUnarchiver,這兩個類是NSCoder的子類,分別用于歸檔以及解檔。下面將介紹如何對自定義對象進(jìn)行歸檔。
對象歸檔示例
現(xiàn)在,我們有一個自定義的Person類,該類有name,age,height三個屬性,其.h文件如下
//Person.h
#import<Foundation/foundation.h>
@interface Person:NSObject<NSCoding>
@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)int age;
@property(nonatomic,assign)double height;
在.m文件中,我們要實現(xiàn)NSCoding中的兩個協(xié)議方法,這兩個方法分別在歸檔和解檔時會被調(diào)用,Person類的.m文件如下
//Person.m
#import"Person.h"
@implementation Person
/*
* 歸檔時調(diào)用該方法,該方法說明哪些數(shù)據(jù)需要儲存,怎樣儲存
*/
- (void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:_name forKey:@"name"];
[encoder encodeInt:_age forKey:@"age"];
[encoder encodeDouble:_name forKey:@"height"];
}
/*
* 歸檔時調(diào)用該方法,該方法說明哪些數(shù)據(jù)需要解析,怎樣解析
*/
-(id)initWithCoder:(NSCoder *)decode
{
if (self = [super init]) {
_name = [decode decodeObjectForKey:@"name"];
_age = [decode decodeIntForKey:@"age"];
_height = [decode decodeDoubleForKey:@"height"];
}
return self;
}
@end
這個Person類就具有了歸檔與解檔能力,當(dāng)你需要對一個Person類的實力對象進(jìn)行儲存或者解析時,在你自己的方法中只要鍵入如下代碼即可,下面兩個方法對應(yīng)兩個按鈕的回調(diào),點擊按鈕時分別執(zhí)行person對象的歸檔和解檔。
//寫操作
- (IBAction)Write {
Person *p = [[Person alloc]init];
p.name = @"jin";
p.age = 10;
p.height = 176.0;
//設(shè)置歸檔后文件路徑
NSString *path = @"/Users/macbookair/Desktop/person.data";
//歸檔
[NSKeyedArchiver archiveRootObject:p toFile:path];
}
//讀操作
- (IBAction)read {
//設(shè)置路徑
NSString *path = @"/Users/macbookair/Desktop/person.data";
//解檔
Person *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"%@--%d---%f",p.name ,p.age ,p.height);
}
對象歸檔特點
- 可以將自定義對象寫入文件或從文件中讀出。
- 由于歸檔時進(jìn)行了加密處理,因此安全性高于屬性列表。
三.CoreData(關(guān)鍵詞:集成化)
什么是CoreData?
當(dāng)你的應(yīng)用程序需要在本地存儲大量的關(guān)系型數(shù)據(jù)模型時,顯然上述方法并不適合,因為不論對象歸檔還是數(shù)據(jù)列表,一旦數(shù)據(jù)模型之間存在依賴關(guān)系,問題就將變得復(fù)雜。而此時iPhone自帶的輕量級數(shù)據(jù)庫Sqlite便成為我們的首選,如果你熟悉數(shù)據(jù)庫,那么恭喜,CoreData也將不再神秘,你可以理解為它是蘋果對Sqlite封裝的一個框架,你可以在Xcode中進(jìn)行Sqlite數(shù)據(jù)庫的可視化操作。倘若你對Sqlite感到陌生,那么本文最后也將對Sqlite進(jìn)行簡單的講解。
如何使用CoreData?
1.新建自帶CoreData文件的工程項目,當(dāng)你創(chuàng)建成功后,左邊文件列表里將看到xxx.xcdatamodeld。并且在AppDelegate類中會自動生成一系列相關(guān)屬性和方法用于管理CoreData。當(dāng)然你也可以為已有項目添加CoreData文件,不過AppDelegate類則需要你重新構(gòu)造相關(guān)方法。
2.點擊xxx.xcdatamodeld文件,你會看到一個CoreData文件相當(dāng)于一個數(shù)據(jù)庫,在這里你可以進(jìn)行建表、添加屬性、連接表與表之間的依賴關(guān)系等可視化操作。但是不能進(jìn)行數(shù)據(jù)的增刪改查,增刪改查需要代碼中自行實現(xiàn)。這里我們創(chuàng)建了名為User(用戶)和Department(部門)兩張表
3.設(shè)置完表的屬性以及關(guān)系后,為每張表創(chuàng)建model。如圖所示,創(chuàng)建完成后左邊文件列表中會多每張表對應(yīng)的兩個文件(iOS7.0以后每張表對應(yīng)四個文件)
4.至此,CoreData基本配置已經(jīng)完成,User表和Department表之間也已經(jīng)建立起聯(lián)系,點擊style可以看到每張表的Attributes及表之間Relationships。
5.如果你熟悉Sqlite,那么接下來我們要做的就是鍵入代碼進(jìn)行數(shù)據(jù)的增刪改查,實現(xiàn)增刪改查之前,我們需要先認(rèn)識幾個核心類。
NSManagedObjectContext: (管理對象上下文) 負(fù)責(zé)應(yīng)用和數(shù)據(jù)庫之間的交互
NSPersistentStoreCoordinator: (持久化存儲協(xié)調(diào)器)處理數(shù)據(jù)存儲的連接
NSManagedObjectModel: (對象模型) 代表CoreData模型文件,相當(dāng)于實體
NSEntityDescription: (實體結(jié)構(gòu)) 用來描述實體
NSPredicate: (查詢條件) 相當(dāng)于Sqlite中的Sql語句
NSFetchRequest: (數(shù)據(jù)請求) 可以給request設(shè)置請求的條件
如何對已經(jīng)建好實體表進(jìn)行增刪改查?最好的方法是自己再封裝一個單利類,該類能夠提供表名查詢方法,傳入某張表名后,我們便可以利用上述的幾個核心類提供的一系列方法創(chuàng)建數(shù)據(jù)的實例變量并讀寫進(jìn)表內(nèi),如果你是新建的工程項目并勾選了Use Core Data選項,那么AppDelegate類會自動生成一些CoreData的管理方法(這些方法在你自己封裝過程中可以借鑒)供我們直接使用。現(xiàn)僅以User表舉例實現(xiàn)增刪改查,以下.m文件里包含了四個xib創(chuàng)建的Button的回調(diào),當(dāng)你點擊按鈕后,分別會向User表增刪改查某個用戶的個人信息。
//ViewController.m
#import "ViewController.h"
#import "User.h"
#import "AppDelegate.h"
#import <CoreData/CoreData.h>
@interface ViewController()
@property(nonatomic,strong)AppDelegate *app;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.app = [UIApplication sharedApplication].delegate;
}
//insert增
- (IBAction)coreDataInsert {
//1.初始化一個user數(shù)據(jù)
User *user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.app.managedObjectContext];
user.name = @"lcc";
user.sex= @"boy";
user.age = @15;
//2.保存
[self.app.managedObjectContext save:nil];
}
//delete刪
- (IBAction) coreDataDelete {
//1.讀取所有用戶
NSEntityDescription *entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:self.app.managedObjectContext];
//2.建立請求
NSFetchRequest *request = [[NSFetchRequest alloc]init];
[request setEntity:entity];
//3.設(shè)置查找條件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name =%@",@"lcc"];
[request setPredicate:predicate];
//4.遍歷user表,找到該用戶后,刪除對象
NSArray *array = [self.app.managedObjectContext executeFetchRequest:request error:nil];
if(array.count){
for(User *newUser in array){
//刪除該用戶
[self.app.managedObjectContext deleteObject:newUser];
}
//保存結(jié)果
[self.app.managedObjectContext save:nil];
NSLog(@"刪除成功");
}else{
NSLog(@"未找到該用戶數(shù)據(jù)");
}
}
//upDate改
- (IBAction) coreDataUpdate {
//1.讀取所有用戶
NSEntityDescription *entity = [NSEntityDescription entityForName:@"User" inManagedObjectContext:self.app.managedObjectContext];
//2.建立請求
NSFetchRequest *request = [[NSFetchRequest alloc]init];
[request setEntity:entity];
//3.設(shè)置查找條件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@",@"lcc"];
[request setPredicate:predicate];
//4.遍歷user表,找到該用戶后,修改對象
NSArray *array = [self.app.managedObjectContext executeFetchRequest:request error:nil];
if(array.count){
for(User *newUser in array){
//修改該用戶
newUser.name = @"lcc2";
}
//保存結(jié)果
[self.app.managedObjectContext save:nil];
NSLog(@"修改成功");
}else{
NSLog(@"未找到該用戶數(shù)據(jù)");
}
}
//select查
- (IBAction) coreDataSelect {
//1.讀取User表
NSEntityDescription *entity = [NSEntityDescription entityForName:@"User"inManagedObjectContext:self.app.managedObjectContext];
//2.建立請求
NSFetchRequest *request = [[NSFetchRequest alloc]init];
[request setEntity:entity];
//3.遍歷所有用戶,取出相關(guān)用戶
NSArray *array = [self.app.managedObjectContext executeFetchRequest:request error:nil];
for (User *user in array){
NSLog(@"%@",user.name);
}
}
@end
如此一來,我們便可以實現(xiàn)已經(jīng)實體化的User表的增刪改查,當(dāng)然,這里的增刪改查相當(dāng)簡便,實際項目中要進(jìn)行各種情況判斷,數(shù)據(jù)庫操作成功失敗會有返回信息,該信息應(yīng)該反饋給用戶。此外,因為CoreData并不是線程安全的,如果你希望自己封裝一個單利類,那么必須要考慮到數(shù)據(jù)庫的并發(fā)操作。
為什么要使用CoreData?
- CoreData脫離了Sql語句,集成化更高。實際上,一個成熟的工程中一定是對數(shù)據(jù)持久化進(jìn)行了封裝的,應(yīng)該避免在業(yè)務(wù)邏輯中直接編寫Sql語句。
- CoreData對版本遷移支持的較好,App升級之后數(shù)據(jù)庫字段或者表有更改會導(dǎo)致crash,CoreData的版本管理和數(shù)據(jù)遷移變得非常有用,手動寫sql語句操作相對麻煩一些。
- CoreData不光能操縱SQLite,CoreData和iCloud的結(jié)合也很好,如果有這方面需求的話優(yōu)先考慮CoreData。
- CoreData是支持多線程的,但需要thread confinement的方式實現(xiàn),使用了多線程之后可以最大化的防止阻塞主線程。
四.Sqlite(關(guān)鍵詞:靈活)
什么是Sqlite?
Sqlite是iPhone自帶的的數(shù)據(jù)庫管理系統(tǒng)。如果你對數(shù)據(jù)庫和Sql語句不陌生,那么在介紹完CoreData后,你一定不滿足CoreData,作為一個程序員,也許你更希望能夠直接操作數(shù)據(jù)庫。既然蘋果選擇Sqlite作為數(shù)據(jù)庫,自然也提供了一系列可以直接操作它的函數(shù)(C語言函數(shù)),利用這些函數(shù)你完全能夠自己封裝一個sqlite數(shù)據(jù)庫框架,但同時你必須熟悉sql語句以及C語言語法。我在gitHub上上傳了一個自己封裝的輕量級Sqlite框架LCCSqliteManager 。目前版本只實現(xiàn)了一些基本的數(shù)據(jù)庫功能,如果感興趣你可以參考一下。
如何直接操作Sqlite?
libsqlite3.0這個函數(shù)庫提供了許多C語言函數(shù)用于直接操作Sqlite,你的項目中導(dǎo)入該函數(shù)庫即可,具體導(dǎo)入方法LCCSqliteManager的文檔中有說明,這里不在過多詳述,僅介紹幾個核心函數(shù)。
/*
** 打開或創(chuàng)建數(shù)據(jù)庫
* char * filePath : 文件路徑 (UTF-8)
* sqlite3 **ppDb : 指向數(shù)據(jù)庫文件指針的指針
*/
int sqlite3_open(const char* filePath, sqlite3** ppDb);
//你可以這樣調(diào)用該函數(shù)
sqlite3 *_sqlite;
NSString *filePath = [NSHomeDirectory()stringByAppendingFormat: @"/Documents/%@.sqlite",filename];
int result = sqlite3_open([filePath UTF8String], &_sqlite);
if (result != SQLITE_OK) {
NSLog(@"error:數(shù)據(jù)庫%@打開失敗",filename)
}
/*
** 關(guān)閉數(shù)據(jù)庫
sqlite3* : 指向該數(shù)據(jù)庫的指針
*/
int sqlite3_close(sqlite3*);
/*
** 創(chuàng)建一張表
*/
SQLITE_API int SQLITE_STDCALL sqlite3_exec(
sqlite3*, /* 非空數(shù)據(jù)庫指針 */
const char *sql, /* 用于創(chuàng)建表的SQL語句 */
int (*callback)(void*,int,char**,char**), /* 回調(diào)函數(shù) */
void *, /* 第一個參數(shù)回調(diào) */
char **errmsg /* 錯誤信息 */
);
//第三、四個參數(shù)一般傳入NULL即可。你可以這樣調(diào)用該函數(shù)
char *error = NULL;
sqlite3 *_sqlite;
NSString *targetSql = @"CREATE TABLE User(name TEXT,age INT,SEX TEXT)"
int result = sqlite3_exec(_sqlite, [targetSql UTF8String], NULL, NULL, &error);
if (result != SQLITE_OK) {
NSLog(@"創(chuàng)建表失敗:%s", error);
return NO;
}
/*
** 預(yù)編譯sql語句
*/
int sqlite3_prepare_v2(
sqlite3 *db, /* 非空數(shù)據(jù)庫指針 */
const char *zSql, /* 要執(zhí)行的SQL語句 */
int nByte, /* zSql字節(jié)的最大長度 */
sqlite3_stmt **ppStmt, /* 能夠使用sqlite3_step()執(zhí)行的編譯好的準(zhǔn)備語句的指針 */
const char **pzTail /* 超過zSql最大長度的的剩余部分*/
);
/*
** 執(zhí)行預(yù)編譯好的sql語句。
*/
int sqlite3_step(sqlite3_stmt*);
/*
** 釋放內(nèi)存。
*/
int sqlite3_finalize(sqlite3_stmt *pStmt);
上述三個方法配套使用,一般在含有查找操作的sql語句時都需要預(yù)編譯后再執(zhí)行,并且手動釋放內(nèi)存。
上面的方法比較常用,表的刪除和修改操作因為都需要查找操作,所以需要sqlite3_prepare_v2 和sqlite3_step配合使用,sqlite3_exec同樣可以執(zhí)行sql語句,因為增加和查找數(shù)據(jù)一般不需要預(yù)編譯。兩者具體的區(qū)別這里不在過多闡述,讀者可以根據(jù)參數(shù)的不同自行理解,或者查找網(wǎng)上有關(guān)libsqlite3.0函數(shù)庫的相關(guān)技術(shù)博客,該函數(shù)庫的大部分函數(shù)以及參數(shù)作用都有詳細(xì)解釋。
Sqlite和CoreData相比的優(yōu)劣
- 直接操作Sqlite更容易理解數(shù)據(jù)的存儲方式,靈活度更高。
- 在大量數(shù)據(jù)的批量讀寫速度上,Sqlite占有優(yōu)勢。
- Sqlite需要自己寫Sql語句,且多線程、批量操作等都需要代碼實現(xiàn)。
總結(jié)
本文大致介紹了iOS的四種數(shù)據(jù)持久化方式,且對他們之間的關(guān)系進(jìn)行了一定講解,筆者在封裝過LCCSqliteManager后,反而更傾向于使用CoreData,因為其更穩(wěn)定便捷,而大部分項目中,NSUSerDefaults與CoreData基本可以滿足數(shù)據(jù)持久化需求,所以筆者較為推薦。
最后感謝閱讀,也歡迎提出寶貴意見或糾正錯誤。