在程序開發中,數據層永遠是程序的核心結構之一。我們將現實事物進行抽象,使之變成一個個數據。對這些數據的加工處理是代碼中能體現技術水平的一大模塊,比如數據的請求、解析、緩存、持久化等等。適當的對數據進行持久化存儲可以實現應用的離線功能,以此提高用戶體驗。在iOS開發中,蘋果提供了四種持久化方案供我們選擇。這些方案分別包括屬性列表(plist)、數據歸檔(NSKeyedValueArchiver/NSUserDefaults)、數據庫(sqlite)和coreData等,它們的區別如下
屬性列表
屬性列表是一種明文的輕量級存儲方式,其存儲格式有多種,最常規格式為XML格式。在我們創建一個新的項目的時候,Xcode會自動生成一個info.plist文件用來存儲項目的部分系統設置。plist只能用數組(NSArray)或者字典(NSDictionary)進行讀取,由于屬性列表本身不加密,所以安全性幾乎可以說為零。因為,屬性列表正常用于存儲少量的并且不重要的數據。
在程序啟動后,系統會自動創建一個NSUserDefaults的單例對象,我們可以獲取這個單例來存儲少量的數據,它會將輸出存儲在.plist格式的文件中。其優點是像字典一樣的賦值方式方便簡單,但缺點是無法存儲自定義的數據。
數據歸檔/序列化
與屬性列表相反,同樣作為輕量級存儲的持久化方案,數據歸檔是進行加密處理的,數據在經過歸檔處理會轉換成二進制數據,所以安全性要遠遠高于屬性列表。另外使用歸檔方式,我們可以將復雜的對象寫入文件中,并且不管添加多少對象,將對象寫入磁盤的方式都是一樣的。
使用NSKeyedArchiver對自定義的數據進行序列化,并且保存在沙盒目錄下。使用這種歸檔的前提是讓存儲的數據模型遵守NSCoding協議并且實現其兩個協議方法。(當然,如果為了更加安全的存儲,也可以遵守NSSecureCoding協議,這是iOS6之后新增的特性)
數據庫
sqlite是一個輕量級、跨平臺的小型數據庫,其擁有可移植性高、有著和MySql幾乎相同的數據庫語句以及無需服務器即可使用的優點:
一、可以存儲大量的數據,存儲和檢索的速度非常快;二、能對數據進行大量的聚合,這樣比起使用對象來進行這些操作要快。
當然,它也具有明顯的缺點:
一、它沒有提供數據庫的創建方式;
二、它基于C語言框架設計,沒有面向對象的API,所以使用起來比較麻煩;
三、復雜的數據模型的數據建表相對而言比較麻煩。
當然,我們也可以使用基于sqlite封裝的開源數據庫FMDB來減少使用sqlite的工作量
coreData
coreData是蘋果官方iOS5之后推出的綜合型數據庫,其使用了ORM(Object Relational Mapping)對象關系映射技術,將對象轉換成數據,存儲在本地數據庫中。coreData為了提高效率,甚至將數據存儲在不同的數據庫中,且在使用的時候將本地數據放到內存中使得訪問速度更快。我們可以選擇coreData的數據存儲方式,包括sqlite、xml等格式。但也正是coreData 是完全面向對象的,其在執行效率上比不上原生的數據庫。除此之外,coreData擁有數據驗證、undo等其他功能,在功能上是四種持久化方案最多的。
上面已經分別介紹了四種方案的優缺點,在開發中,并沒有說哪種持久化方案是最好的,只能說在不同開發場景下,最適合使用的持久化方案。下面我們將用代碼實戰的方式對這些持久方案進行更加詳細的了解
屬性列表
在我們每次創建新的項目的時候,Xcode幫助我們生成了Info.plist文件,里面存儲了關于項目名字、版本、bundle id等等關鍵信息,這個plist文件也是逆向工程(越獄)中獲取app數據的重要文件。OK,那么什么情況下用plist存儲呢?打個比方,最近在實現公司項目業務的時候,需要使用選擇器(UIPickerView)給用戶選擇所在城市。對于城市數據,并沒有加密的必要,而且這時候使用plist會達到更高一些的效率。既然已經知道需要的數據,那么很容易就得得出省-市這樣的一對多的數據類型,我們的plist使用字典,將省份作為key,存儲對應的城市的數組作為value
1、創建plist文件。New Files -> iOS -> Resource -> Property List -> Next
2、給這個plist文件命名cities,點擊Create后創建好。然后我們選中Root,默認已經是字典的數據結構存儲了,我們點擊Root右邊的加號添加一些鍵值對,然后修改左邊的key的文字,并且將每一個key對應的value設置為Array數組
3、按照在字典中添加鍵值對的方式,在設置好每個key對應的類型之后,移動到每一行上面點擊出現的+給每一個省份添加數組元素,并且賦值,最終效果圖如下
當然,像這種城市的plist文件百度一下就可以找到,但是創建plist的方式相信大家看完之后也就明白了。從plist讀取數據的方式也很簡單,蘋果把讀取的方法封裝在NSArray跟NSDictionary中,讀取步驟分為兩步:
1、獲取plist文件路徑:
NSString * filePath = [[NSBundle mainBundle] pathForResource: @"cities" ofType: @"plist"];
2、通過數組或者字典的構造器方法創建容器:
NSDictionary * dict = [NSDictionary dictionaryWithContentsOfFile: filePath];
//NSArray * array = [NSArray arrayWithContentsOfFile: filePath];
實現選擇器的大概思路是用兩個數組分別存儲省份以及當前選中省份的城市數組,然后在滑動pickerView的回調事件中根據選中的省份更新城市數據源。
另外,還有一個NSUserDefault,其支持的數據格式有:NSNumber(Integer、Float、Double等)、NSString、NSDate、NSArray(成員必須也是支持的格式類型)、NSDictionary(同NSArray)。其使用和讀取也是非常的簡單,像字典一樣的存取方式
讀取:NSString * str = [[NSUserDefaults standardUserDefaults] valueForKey: @"str"];
存儲:[[NSUserDefaults standardUserDefaults] setValue: @"str" forKey: @"str"];
同樣的也可以使用setObject:forKey:或者objectForKey:等字典存取方法
數據歸檔/數據序列化
更多時候,NSUserDefaults已經提供了存儲簡單變量的持久化方案。然而,當我們想要存儲復雜的自定義數據時,NSUserDefaults無法為我們提供更多的幫助,這時候考慮的是另外的持久化方案。而并非所有的程序都需要查詢數據、數據遷移這些操作,而且也并非所有的數據都有復雜的關系圖。這時候,數據歸檔絕對是不二的選擇。
我們使用archive格式的文件將歸檔化的數據存儲在沙盒目錄下,這種格式的文件讀取出來是二進制數據(NSData),然后使用NSKeyedUnarchiver類對數據進行反序列化。假設當前需要進行持久化存儲的是一款離線游戲,我需要存儲游戲前十名的成績、成績持有者和記錄創建時間這些數據,那么相對應的LXDGameRecord類聲明如下,其必須遵循NSCoding或NSSecureCoding協議之一:
@interface LXDGameRecord : NSObject<NSCoding>
@property (nonatomic, copy) NSString * userName;
@property (nonatomic, strong) NSDate * createDate;
@property (nonatomic, strong) NSNumber * score;
@end
@implementation LXDGameRecord
#pragma mark - NSCoding
/** 協議方法-對數據進行反序列化并讀取*/
- (id)initWithCoder: (NSCoder *)aDecoder
{
? ? self.userName = [aDecoder decodeObjectForKey: kUserNameKey];
? ? self.createDate = [aDecoder decodeObjectForKey: kCreateDateKey];
? ? self.score = [aDecoder decodeObjectForKey: kScoreKey];
}
/** 協議方法-對數據進行序列化*/
- (void)encodeWithCoder:(NSCoder *)aCoder
{
? ? [aCoder encodeObject: self.userName forKey: kUserNameKey];
? ? [aCoder encodeObject: self.createDate forKey: kCreateDateKey];
? ? [aCoder encodeObject: self.score forKey: kScoreKey];
}
@end?
對于任意自定義類型的數據,只要遵循上面的步驟,就能對數據進行歸檔了。這里還要講一下一個小技巧:使用static修飾來替代宏定義。上面的序列化中,我們可以看到NSCoding的協議方法中對數據進行序列化并且使用一個key來保存它。正常情況下我們可以使用宏來定義key,但是過多的宏定義在編譯時也會造成大量的損耗。這時候可以使用static定義靜態變量來取代宏定義。
static NSString * const kUserNameKey = @"userName";
讓自定義的數據遵循NSCoding協議后,我們就能使用NSKeyedArchiver和NSKeyedUnarchiver來對持久化的數據進行存取操作了
/** 使用NSKeyedUnArchiver對數據反序列化并讀取*/
NSString * filePath = [self applicationDocumentStorage];
NSData * fileData = [NSData dataWithContentFile: filePath];
NSKeyedUnarchiver * unarchiver = [[NSKeyedArchiver alloc] initForReadingWithData: fileData];
NSArray * datas = [unarchiver decodeObjectForKey: kArchiveKey]; ? //反序列化
[unarchiver finishDecoding]; ?//完成反序列化
/** 使用NSKeyedArchiver對數據進行序列化*/
NSMutableData * recordData = [NSMutableData data];
NSKeyedArchiver * archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: recordData];
[archiver encodeObject: datas forKey: kArchiveKey]; ?//序列化數據
[archiver finishEncoding]; ?//完成序列化操作
[recordData writeToFile: filePath]; ?//序列化完成后寫入本地磁盤
下面展示的是曾經學習時仿qq登錄界面中使用數據歸檔存儲用戶注冊的賬號(由于沒有服務器,只能存儲在本地),其中用戶的數據包括了昵稱、頭像圖片、用戶賬號、密碼這些數據,在輸入完賬號自己從代理方法中訪問這些數據并獲取用戶的頭像
sqlite數據庫
基于sqlite封裝的FMDB幾乎是我工作中最常用到的持久化方案。在實際開發中,sqlite占用的內存非常非常的少,在嵌入式設備中,可能只需要幾百K即可。其次,它的速度非常的快,幾乎快過所有其他的數據庫。當然啦,開始使用數據庫進行開發之前,你得了解sqlite支持的數據類型,包括NULL(空值)、Integer(整型)、Real(實數)、Text(字符串)、BLOB(二進制)
在使用sqlite前要導入libsqlite3.0框架,然后導入<sqlite3.h>頭文件。其操作步驟大致如下:
1、使用sqlite3_open(const char *filename, sqlite3 **ppDb)方法打開指定路徑下的數據庫存入到創建的數據庫變量中,如果存在數據庫就打開。不存在數據庫則關閉。成功打開數據庫的時候會返回SQLITE_OK
static NSString * const dbName = @"myDBText.db";
NSString * documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString * filePath = [documentDirectory stringByAppendingPathComponent: dbName];
if (SQLITE_OK == sqlite3_open(filePath.UTF8String, &_database)) {
NSLog(@"數據庫打開成功!");
} else {
NSLog(@數據庫打開失敗!“);
}
2、執行SQL語句。有返回值的語句執行第四步,無返回值的執行第三步
3、對于無返回值的SQL語句(包括增刪改等操作)通過sqlite3_exec()函數執行
char * error;
NSString * executeSql = @"select * from table student where name like '張%'";
if (SQLITE_OK != sqlite3_exec(_database, executeSql.UTF8String, NULL, NULL, &error)) {
NSLog(@"執行SQL語句過程發生錯誤!");
}
4、對于有返回值的SQL語句首先通過sqlite3_prepare_v2()進行語法評估,然后通過sqlite3_step()函數依次取出查詢結果的每一行數據,最后通過sqlite3_column_type()方法獲取對應列的數據
NSMutableArray * rows = [NSMutableArray new];
sqlite3_stmt * stmt;? ? ? ? //檢查語法正確性
if (SQLITE_OK == sqlite3_prepare_v2(_database, executeSql.UTF8String, -1, &stmt, NULL)) {
//單步執行sql語句
while (SQLITE_ROW == sqlite3_step(stmt)) {
int columnCount = sqlite3_column_count(stmt);? //獲取列數
NSMutableDictionary * dic = [NSMutableDictionary dictionary];
for (int i = 0; i < columnCount; i++) {
const char * name = sqlite3_column_name(stmt, i);? //取到列名
const unsigned char * value = sqlite3_column_text(stmt, i); //取得某列的值
dic[[NSString stringWithUTF8String: name]] = [NSString stringWithUTF8String: (const char *)value];
}
[rows addObject: dic];
}
}
sqlite3_finalize(stmt);
sqlite的原生語句對于開發者而言有時是一個災難,在熟練使用之前,我們很難保證數據庫的語句和執行代碼沒有任何問題。對此,我們可以在github上面找到基于sqlite3封裝的FMDB,它提供了特有的機制來保證數據庫訪問是線程安全的,至于使用方法在網上一搜一大把教程,這里就不在細說。但是,使用FMDB有一個要注意的問題是——當我們把圖片轉換成二進制數據存儲在數據庫中的時候,再次讀取出這個二進制數據初始化成圖片的時候會出錯誤,無法正常轉換成圖像。解決方案詳見這里http://mobile.51cto.com/hot-405287.htm
coreData
coreData是iOS5之后蘋果推出的數據持久化框架,其提供了ORM的功能,將對象和數據相互轉換。其中,它提供了包括sqlite、xml、plist等本地存儲文件,默認使用sqlite進行存儲。coreData具有兩個模型:關系模型和對象模型,關系模型即是數據庫,對象模型為OC對象。其關系圖如下
由于我們不需要關心數據的存儲,coreData使用起來算是最簡單的持久化方案。要使用coreData有兩個方式,一個是在創建項目的時候勾選use core data,另一個則是手動創建。在這里我們要講解的是前者創建的方式
1、創建新項目勾選使用coreData
2、創建關系模型,在這里我創建的模型名字是LXDCoreDataDemo
3、在創建的關系模型中添加實體,命名為Person,并且添加三個字段:name、age、score
到了這里我們的實體模型就創建好了,接下來就是通過NSManagedObject來將實體模型轉換成對象。通過從coreData取出的對象,全部都是繼承自NSManagedObject的子類。那么我們需要根據當前的關系模型來創建Person類
選擇LXDCoreDataDemo -> Next -> Person -> Create,我們就創建好了Person,這時候三個成員屬性都會自動添加完成
接著我使用故事板創建了下面的視圖,在我點擊按鈕的時候往數據庫中插入新的person數據
在執行操作的類實現文件中,我們要加入AppDelegate和Person的頭文件,因為在創建項目的時候如果我們勾選了use core data的選項,appDelegate文件中會幫我們生成用于管理、存儲這些模型的對象,我們可以通過添加頭文件來使用。插入數據的代碼如下:
//先取出coredata上下文管理者
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = appDelegate.managedObjectContext;
//保存新數據
Person *person = [NSEntityDescription insertNewObjectForEntityForName: @"Person" inManagedObjectContext: context];
person.name = _userName.text;
person.name = _userScore.text;
person.age = @([_userAge.text integerValue]);
[appDelegate saveContext];
//查詢所有數據
NSError *error;
NSFetchRequest *request = [NSFetchRequest new];
NSEntityDescription *entity = [NSEntityDescription entityForName: @"Person" inManagedObjectContext: context];
[request setEntity: entity];
NSArray *results = [[context executeFetchRequest: request error: &error] copy];
for (Person *p in results) {
NSLog(@"%@, %@, %@", p.name, p.age, p.score);
}
想要對coreData有更深入的了解可以購買這本Core Data應用開發實踐指南,里面詳細講述了coreData的各種使用技巧。
對于我們開發者而言,使用適當的持久化方案可以幫助我們獲得更高的開發效率。更快的數據加載速度可以顯著提高應用的用戶體驗感。
文集:iOS開發
轉載注明鏈原文地址以及作者