數據持久化的相關知識
四種將數據持久化存儲到iOS文件系統的機制:
- plist
- 對象歸檔
- iOS的嵌入式關系數據庫SQLite3
- Core Data
每個應用都有自己的/Documents文件夾,且僅能讀寫各自的/Documents目錄中的內容。
iOS應用的3個支持文件夾:
- Documents:應用將數據存儲在Documents中,但基于NSUserDefaults的首選項設置除外。
- Library: 基于NSUserDefaults的首選項設置存儲在Library/Preferences文件夾中
- tmp: tmp目錄供應用存儲臨時文件,當iOS設備執行和iTunes的同步時,不會備份其中的tmp文件,但在不需要這些文件時,應用要負責刪除tmp中的文件,以免占用文件系統空間。
獲取Documents目錄
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = paths[0];
常量NSDocumentDirectory表明我們正在尋找Documents目錄的路徑。第二個常量 NSUserDomainMask表明我們希望將搜索限制在應用的沙盒里。雖然返回的是一個匹配路徑的數組,但我們知道數組中位于索引0處的一定是Documents目錄,為什么呢?我們知道每個應用只有一個Documents目錄,因此只有一個目錄符合指定的條件。
可以在剛剛檢索到的路徑的結尾附加另一個字符來創建文件名。為此要使用專為該目的設計的NSString方法,即stringByAppendingPathComponent:方法,如下所示:
NSString *fileName = [documentsDirectory stringByAppendingPathComponent:@"theFile.txt"];
完成此調用之后,fileName就是指向應用的Documents目錄中theFile.txt文件的完整路徑,然后我們就可以使用fileName來創建、讀取和寫入文件了。
獲取tmp目錄
獲取對應用臨時目錄的引用比獲取對 Documents目錄的引用容易。名為NSTemporaryDirectory()的Foundation函數將返回一個字符串,該字符串包含到應用臨時目錄的完整路徑。若要創建一個將會存儲在臨時目錄中的文件,首先要找到該臨時目錄:
NSString *tmpPath = NSTemporaryDirectory();
然后,在路徑的結尾附上文件名就可以創建指向該目錄下文件的路徑,比如:
NSString *tempFile = [tmpPath stringByAppendingPathComponent:@"theFile.txt"];
文件保存方案
單文件持久化
將數據保存在一個文件中是最簡單的方法,且對于許多應用,這也是完全可以接受的方法。首先,創建一個根對象,通常是NSArray或NSDictionary(使用歸檔文件的情況下根對象可以基于某個自定義類)。接下來,使用所有需要保存的程序數據填充根對象。真正保存的時候,代碼會將該根對象的全部內容重新寫入單個文件。應用在啟動時會將該文件的全部內容讀入內存,并在退出時注銷。
使用單文件的缺點是必須將全部數據加載到內存中,并且不管更改多少也必須將所有數據全部重新寫入文件系統。如果應用管理的數據不超過幾兆字節,此方法可能非常好,且簡單。
多文件持久化
使用多個文件是另一種實現持久化的方法。太復雜!
plist
使用屬性列表非常方便,因為可以使用Xcode或Property List Editor應用手動編輯他們,并且只要字典或數組包含特定可序列化對象,就可以將NSDictionary和NSArray 實例寫入屬性列表或者從屬性列表創建它們。
屬性列表序列化
序列化對象(serialized object)是指可以被轉換為字節流以便于存儲到文件中或通過網絡進行傳輸的對象。雖然說任何對象都可以被序列化,但只有某些特定對象才能被放置到某個集合類中(字典,數組),然后才使用該集合類的writeToFile:atomically:方法或writeToURL:atomically:方法將它們存儲到屬性列表中??梢园凑赵摲椒ㄐ蛄谢旅娴腛bjective-C類:NSArray,dictionary,data,string及它們的可變子類,NSNumber,NSDate
這里的atomically參數讓該方法將數據寫入輔助文件,而不是寫入指定位置。成功寫入該文件之后,輔助文件將被復制到第一個參數指定的位置。這是更安全的做法,因為如果應用在保存期間崩潰,則現有文件不會被破壞。
練習使用
創建一個outlet集合:
@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;
在storyboard中,按住control從頂部的view controller圖標拖到要對應的每個集合成員上,并選擇集合名字。
用于確定文件路徑:
- (NSString *)dataFilePath{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = paths[0];
return [documentsDirectory stringByAppendingPathComponent:@"data.plist"];
}
在viewDidLoad中:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSArray *array = [[NSArray alloc] initWithContentsOfFile:filePath];
for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];
theField.text = array[i];
}
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
// Do any additional setup after loading the view, typically from a nib.
}
檢查數據文件是否存在,如果不存在就不加載它了,如果存在,就用該文件內容實例化數組,然后將數組中的對象復制到四個文本框中。由于數組是按順序排列的列表,因此只要根據保存順序(之后會有)來復制數組,就一定能確保相應地字段獲得正確的值。
-
從屬性列表中加載數據后,我們獲得了對應用實例的引用,并使用該引用訂閱UIApplicationWillResignActiveNotification通知,這是由UIApplication定義的字符串常量,最后一個參數app是sender.
- (void)applicationWillResignActive:(NSNotification *)notification{ NSString *filePath = [self dataFilePath]; //對outlet集合中每個元素提取相同的屬性(KVC) NSArray *array = [self.lineFields valueForKey:@"text"]; [array writeToFile:filePath atomically:YES]; }
最后一個方法是applicationWillResignActive:。注意它接受一個指向NSNotification的指針作為參數。它是一個通知方法,所有通知都接受一個NSNotification實例作為參數。應用應該在終止運行或者進入后臺之前保存數據,所以我們需要使用名為UIApplicationWillResignActiveNotification通知。這樣,只要這個應用不再是當前正在與用戶進行交互的應用,就會發布通知,包括用戶按下Home鍵,來了個電話。
通知方法通過調用lineFiedls數組中每個文本框的text方法構建一個字符串數組。我們利用了一個便捷的方法,沒有迭代數組中的文本框,而是用了valueForKey:方法,并傳遞@"text"作為參數。NSArray類的valueForKey:方法為我們實現了迭代獲取實例變量的text值,返回包含這些值的數組。然后我們將該數組的內容寫入一個屬性列表文件中。
對模型對象進行歸檔
在Cocoa世界中,歸檔(archiving)是指另一種形式的序列化,但它是任何對象都可以實現的更常規的類型。專門編寫用于保存數據的任何模型對象都應該支持歸檔。使用對模型對象進行歸檔的技術可以輕松將復雜的對象寫入文件,然后再從中讀取它們。
只要在類中實現的每個屬性都是標量(int.float)或是遵循NSCoding協議的某個類的實例,你就可以對整個對象進行完全的歸檔。由于大多數支持存儲數據的Foundation和Cocoa Touch類都遵循NSCoding協議(除UIImage)。
還有一個協議應該和NSCoding協議一起實現,那就是NSCopying協議。后者允許復制對象,這使你在使用數據模型對象時具備了較大的靈活性。
遵循NSCoding協議
NSCoding協議聲明了兩個方法,這兩個方法都是必需的。一個方法將對象編碼到歸檔中,另一個方法對歸檔解碼來創建一個新對象。這兩個方法都傳遞一個NSCoder實例,使用方法和NSUserDefaults相似。也可以使用KVC對對象和原生數據類型(int,float)進行編碼和解碼。
對某個對象進行編碼的方法可能看起來如下:
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:foo forKey:kFooKey];
[aCoder encodeObject:bar forKey:kBarKey];
[aCoder encodeInt:someInt forKey:kSomeIntKey];
[aCoder encodeFloat:someFloat forKey:kSomeFloat];
}
若要我們在項目中支持歸檔,必須使用正確的編碼方法將所有實例變量編碼成encoder。如果要子類化某個也遵循NSCoding的類,還需要確保對超類調用encodeWithCoder:方法,你的方法將如下所示:
- (void)encodeWithCoder:(NSCoder *)aCoder{
[super encodeWithCoder:aCoder];
[aCoder encodeObject:foo forKey:kFooKey];
[aCoder encodeObject:bar forKey:kBarKey];
[aCoder encodeInt:someInt forKey:kSomeIntKey];
[aCoder encodeFloat:someFloat forKey:kSomeFloat];
}
我們還需要實現一個通過NSCoder解碼的對象初始化方法,恢復我們之前歸檔的對象。實現initWithCoder:方法比實現encodeWithCoder:方法稍微復雜一些。如果直接對NSObject進行子類化,或者對某些不遵循NSCoding的其他類進行子類化,則你的方法看起來如下
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
foo = [aDecoder decodeObjectForKey:kFooKey];
bar = [aDecoder decodeObjectForKey:kBarKey];
someInt = [aDecoder decodeObjectForKey:kSomeIntKey];
someFloat = [aDecoder decodeObjectForKey:kAgeKey];
}
return self;
}
該方法使用[super init]初始化對象實例,如果初始化成功,則它通過解碼NSCoder的實例中傳遞的值來設置其屬性。當為某個具有超類且遵循NSCoding的類實現NSCoding時,initWithCoder:方法應稍有不同。它不再對super調用init,而是調用initWithCoder,像這樣:
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super initWithCoder:aDecoder]) {
foo = [aDecoder decodeObjectForKey:kFooKey];
bar = [aDecoder decodeObjectForKey:kBarKey];
someInt = [aDecoder decodeObjectForKey:kSomeIntKey];
someFloat = [aDecoder decodeObjectForKey:kAgeKey];
}
return self;
}
只要實現這兩個方法,就可以對所有對象的屬性進行編碼和解碼,然后便可以對對象進行歸檔,并且可以將其寫入歸檔或者從歸檔中讀取它們。
NSCopying協議
如前所述,遵循NSCopying對于任何數據模型對象來說都是非常好的事情。NSCopying有一個copyWithZone:方法,可用來復制對象。實現NSCopying和實現initWithCoder:非常相似,只需創建一個同一類的新實例,然后將該新實例的所有屬性都設置為與該對象屬性相同的值。此處的copyWithZone:方法的內容類似下:
- (instancetype)copyWithZone:(NSZone *)zone{
MyClass *copy = [[[self class] allocWithZone:zone] init];
copy.foo = [self.foo copyWithZone:zone];
...
return copy;
}
對數據對象進行歸檔和取消歸檔
從遵循NSCoding的一個或多個對象創建歸檔相對比較容易。首先創建一個NSMutableData實例,用于包含編碼的數據。然后創建一個NSKeydArchiver實例,用于將對象歸檔到此NSMutableData實例中:
NSMutableData *data = [NSMutableData new];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
創建這兩個實例后,我們使用KVC來對希望包含在歸檔中的所有對象進行歸檔,像這樣:
[archiver encodeObject:myObject forKey:@"keyValueString"];
對所有要包含的對象進行編碼之后,我們只需告知歸檔程序已經完成了這些操作。將NSMutableData實例寫入文件系統:
[archiver finishEncoding];
BOOL success = [data writeToFile:@"/../.." atomically:YES];
寫入文件時出現錯誤會將success設置為NO.如果success為YES,則數據已成功寫入指定文件。從該歸檔創建的任何對象都將是過去寫入該文件的對象的精確副本。
從歸檔重組對象的步驟類似。從歸檔文件創建一個NSData實例,并創建一個NSKeyedUnarchiver以對數據進行解碼:
NSData *data = [[NSData alloc] initWithContentsOfFile:@"/.../.."];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
然后,使用之前對對象進行歸檔的同一個鍵從解壓程序中讀取對象。
self.object = [unarchiver decodeObjectForKey:@"key"];
最后,告知歸檔程序完成了該操作:
[unarchiver finishDecoding];
在某個需要歸檔的類中:
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.lines = [aDecoder decodeObjectForKey:kLinesKey];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.lines forKey:kLinesKey];
}
- (instancetype)copyWithZone:(NSZone *)zone{
BIDFourLines *copy = [[[self class] allocWithZone:zone] init];
NSMutableArray *linesCopy = [NSMutableArray new];
for (id line in self.lines) {
linesCopy addObject:[line copyWithZone:zone];
}
copy.lines = linesCopy;
return copy;
}
我們剛才實現了遵循NSCoding和NSCopying所需的所有方法。在encodeWithCoder:中對四個屬性進行了編碼,并在initWithCoder:中使用相同的4個鍵值對對這些屬性進行解碼。在copyWithZone:中,我們創建了一個新的BIDFourLines對象,并將四個字符串復制到其中。
然后更改ViewController。
- (void)viewDidLoad {
[super viewDidLoad];
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSData *data = [[NSMutableData alloc] initWithContentsOfFile:filePath];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
BIDFourLines *fourlines = [unarchiver decodeObjectForKey:kRootKey];
[unarchiver finishDecoding];
for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];
theField.text = fourlines.lines[i];
}
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)applicationWillResignActive:(NSNotification *)notification{
NSString *filePath = [self dataFilePath];
//對outlet集合中每個元素提取相同的屬性(KVC)
BIDFourLines *fourLines = [[BIDFourLines alloc] init];
fourLines.lines = [self.lineFields valueForKey:@"text"];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:fourLines forKey:kRootKey];
[archiver finishEncoding];
[data writeToFile:filePath atomically:YES];
}
使用內嵌的SQLite3
首先在Targets的build phase中引入sqlite3動態庫
SQLite3在存儲和檢索大量數據方面非常有效。他還能夠對數據進行復雜的聚合,與使用對象執行這些操作比,獲得結果的速度更快。
SQLite3可以不需要將所有對象加載到內存中。
SQLite3使用SQL(Structured Query Language, 結構化查詢語言)。SQL是與關系數據庫交互的標準語言。
關系數據庫和面向對象的編程語言使用完全不同的方法來存儲和組織數據。這些方法差異很大,因而出現了在兩者之間進行轉換的各種技術以及很多庫和工具。這些技術統稱為ORM(Object-Relational Mapping,對象關系映射)。目前有很多ORM工具可用于Cocoa touch.實際上,Apple提供的Core Data就是一種。
創建和打開數據庫
使用SQLite3前,必須打開數據庫。用于執行此操作的命令是sqlite3_open().這樣將打開一個現有數據庫,如果指定位置上不存在數據庫,則函數便會創建一個新的數據庫,下面是打開新數據庫的代碼
sqlite3 *database;
int result = sqlite3_open("/path/to/database/file", &database);
如果result等于常量 SQLITE_OK,就表示數據庫已經打開。此處你應該記住,數據庫文件必須以C字符串而非NSString的形式進行傳遞。SQLite3是采用可移植的C而非OC編寫的,它不知道什么是NSString.所幸,NSString有個方法,該方法能將NSString實例轉化為C字符串。
const char *stringPath = [@"string" UTF8String];
對SQLite3數據庫執行完所有操作后,調用一下內容來關閉數據庫。
sqlite3_close(database);
數據庫將所有數據存在表中??梢酝ㄟ^SQL的CREATE語句創建一個新表,并使用sqlite3_exec將其傳遞到打開的數據庫,代碼如下:
char *errorMsg;
const char *createSQL = "CREATE TABLE IF NOT EXISTS PEOPLE""(ID INTEGER PRIMARY KEY AUTOINCREMENT, FIELD_DATA TEXT)";
int result = sqlite3_exec(database, createSQL, NULL, NULL, &errorMsg);
如果里那個字符之間除了空白(包括換行符)之外沒有其他分隔字符,那么這兩個字符串會被連接成一個字符串.
如之前所做的一樣,需要檢查result是否等于SQLITE_OK以確保命令成功運行。如果命令未成功運行,errorMsg將對所發生的問題進行描述
函數sqlite3_exec()針對SQLite3運行任何不返回數據的命令。它用于執行:
- 更新
- 插入
- 刪除
從數據庫中檢索數據有點復雜,必須首先向其輸入SQL的SELECT命令來準備該語句
NSString *query = @"SELECT ID, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *statement;
int result = sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil);
所有接受字符串的SQLite3函數都要求使用舊式的C字符串。在實例中,我們可以創建并傳遞一個C字符串,也可以創建一個NSString并通過它的方法UTF8String派生一個C字符串。這兩個方法都行。如果需要操縱字符串,則使用NSString或NSMutableString較為容易,但將NSString轉換為C字符串會導致一些額外的開銷。
如果result等于SQLITE_OK,則語句準備成功,可以開始遍歷結果集。下面的例子將遍歷結果集從數據庫中搜索int 和 NSString。
while (sqlite3_step(statement) == SQLITE_ROW) {
int rowNum = sqlite3_column_int(statement, 0);
char *rowData = (char *)sqlite3_column_text(statement, 1);
NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
//do something with the data here
}
sqlite3_finalize(statement);
綁定變量
雖然可以通過創建SQL字符串來插入值,但常用的方法是使用綁定變量(bind variable)來執行數據庫插入操作。正確處理字符串并確保他們沒有無效字符(以及引號處理過的屬性)是非常繁瑣的事情。借助綁定變量,這些問題都迎刃而解。
需使用綁定變量插入值,只需按正常方式創建SQL語句,但要在SQL字符串中添加一個問號.每個問號都表示一個需要在語句執行之前進行綁定的變量。然后準備好SQL語句,將值綁定到各個變量并執行命令。
下面這個示例使用兩個綁定變量預處理SQL語句,它將int綁定到第一個變量,將字符串綁定到第二個變量,然后執行查詢語句:
char *sql = "insert into foo values (?, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, sql, -1, &stmt, nil) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, 235);
sqlite3_bind_text(stmt, 2, "Bar", -1, NULL);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
NSLog(@"This should be real error checking");
}
sqlite3_finalize(stmt);
根據希望使用的數據類型,可以選擇不同的綁定語句。大部分綁定函數都只有3個參數。
- 無論針對哪種數據類型,任何綁定函數的第一個參數都指向之前在sqlite3_prepare_v2()調用中使用的sqlite3_stmt.
- 第二個參數是所綁定的變量的索引。它是一個有序索引的值,這表示SQL語句中的第一個問號索引是1,而其后每個問號的索引值都依次加1
- 第三個參數始終表示應該替換問號的值。有些綁定函數(比如說用于綁定文本和二進制數據的綁定函數)擁有另外兩個參數。
- 一個參數是在上面的第三個參數中傳遞的數據的長度。對于C字符串,可以傳遞-1來代替字符串的長度,則函數將使用整個字符串,對于所有其他情況,需要指定所傳遞數據的長度。
- 另一個參數是可選的函數callback,用于在語句執行后完成內存清理工作。通過整個函數使用malloc()釋放已分配的內存。
- 綁定函數后面的語法似乎看起來有點奇怪,因為我們執行了一個插入操作。當使用綁定常量時,會將相同語法同時用于查詢和更新。如果SQL字符串包含了一個SQL查詢(而不是更新),我們需要多次調用sqlite3_step(),直到它返回SQLITE_DONE。因為這里是更新,所以僅調用一次。
SQLite3的應用
#import "ViewController.h"
#import <sqlite3.h>
@interface ViewController ()
@property (copy, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
sqlite3 *database;
if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Failed to open database");
}
NSString *createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS""(ROW INTEGER PRIMARY KEY, FIELD_DATA TEXT)";
char *errorMsg;
if (sqlite3_exec(database, [createSQL UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Error creating table: %s", errorMsg);
}
NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, [query UTF8String], -1, &stmt, nil) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
int row = sqlite3_column_int(stmt, 0);
char *rowData = (char *)sqlite3_column_text(stmt, 1);
NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
UITextField *field = self.lineFields[row];
field.text = fieldValue;
}
sqlite3_finalize(stmt);
}
sqlite3_close(database);
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
}
- (void)applicationWillResignActive:(NSNotification *)notification{
sqlite3 *database;
if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Failed to open database");
}
for (int i = 0; i < 4; i++) {
UITextField *field = self.lineFields[i];
char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA""VALUES (?, ?);";
char *errorMsg = NULL;
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, i);
sqlite3_bind_text(stmt, 2, [field.text UTF8String], -1, NULL);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
NSAssert(0, @"Error updating table: %s", errorMsg);
}
sqlite3_finalize(stmt);
}
sqlite3_close(database);
}
- (NSString *)dataFilePath{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = paths[0];
return [path stringByAppendingPathComponent:@"data.sqlite"];
}
@end
首先打開數據庫,如果在打開時遇到了問題,則關閉它并拋出一個斷言錯誤。
接下來,需要確保有一個表來保存我們的數據??梢允褂肅REATE TABLE來完成此任務。通過指定IF NOT EXISTS,可以防止數據庫覆蓋現有數據。如果已有一個具有相同名稱的表,此命令會直接退出,不執行任何操作,所以可以在應用每次啟動時安全地調用它,無需顯式檢查表是否存在。
最后需要加載數據。為此,使用SELECT語句。在這個簡單例子中,我們創建了一個SELECT來從數據庫請求所有行,并要求SQLite3準備我們的SELECT。要告訴SQLite3按行號排序各行,以便我們總是以相同順序獲取它們。否則,SQLite3將按內部存儲順序返回各行。
最后關閉數據庫連接,所有操作到此結束。
請注意,我們在創建表和加載它所包含的所有數據后立即關閉了數據庫連接,而不是在應用運行的整個過程中保持打開狀態。這是管理連接最簡單的方式,對于整個小應用,我們可以在需要連接時再打開它。在其他需要頻繁使用數據庫的應用中,可能有必要始終打開連接。
其他更改是在applicationWillResignActive:方法中進行的,我們需要把應用數據保存在這里。由于數據庫中的數據存儲在一個表中,存儲后應用的數據看起來跟下面相似:
行 | FIELD_DATA
------------- | -------------
0 | text1
1 | text2
2 | text3
3 | text4
applicationWillResignActive:方法會首先再次打開數據庫。然后保存數據,在4個字段中進行循環,生成4條獨立命令來更新數據庫中的每一行。
我們在循環中要做的第一件事就是創建一個字段名稱,以便可以檢索到正確的文本框輸出口。記住,使用valueForKey:可以根據名稱檢索屬性。同時為錯誤消息聲明一個指針,在出現錯誤時使用。
我們設計了一條帶兩個綁定變量的 INSERT OR REPLACE的SQL語句。第一個變量代表所存儲的行,第二個變量代表要存儲的實際內容字符串值。使用INSERT OR REPLACE而不是更標準的INSERT,就不必擔心某個行是否存在。
接下來聲明一個指向語句的指針,然后為語句添加綁定變量,并將值綁定到兩個綁定變量
然后調用sqlite3_step()來執行更新,檢查并確定其運行正常,然后完成語句,結束循環。
注意,此處使用了一個斷言來檢查錯誤條件。之所以會使用斷言,而不使用異?;蚴謩渝e誤檢查,是因為這種情況只有在開發人員出錯的情況下才會出現。使用此斷言宏有助于我們調試代碼,并且可以脫離最終應用。如果某個錯誤條件是用戶正常情況下可能遇到的條件,則應該使用其他形式的錯誤檢查。
有一個條件可能導致前面的SQLite代碼出現錯誤,而不是程序員錯誤。如果設備的存儲區已滿,SQLite無法將其更改保存到數據庫,那么這里也會發生錯誤。但是這種情況很少見,并可能為用戶帶來更深層次的問題,不過這已超出了應用數據的范圍。
完成循環后,關閉數據庫。
Core Data
Core Data是一款穩定,功能全面的持久化工具。
你不需要創建類,而是先在數據模型編輯器中創建一些實體(entity),然后在代碼中為這些實體創建托管對象.(managed object)
實體表示對對象的描述,而托管對象表示在運行時創建的該實體的具體實例,
因此,在數據模型編輯器中,你將創建實體,而在代碼中,你將創建并檢索托管對象。實體和托管對象之間的差異類似于類和類的實例。
實體由屬性組成,屬性分為3種類型。
- 特性(attribute) 特性在Core Data實體中的作用與實例變量在Objective-C類中的作用完全相同,它們都用于保存數據。
- 關系--顧名思義,關系用于定義實體之間的關系。舉例來說,假設要定義一個Person實體,你可能首先會定義一些特性,比如hairColor,eyeColor,height和weight.你還可以定義地址特性,比如說state和zipCode.或者,可以將他們嵌入到單獨的HomeAddress實體中。時候后面這種方法,你可能還希望在Person和HomeAddress之間創建一個關系。關系可以1對1或1對多。從Person到HomeAddress的關系可以是1對1,因為大多數人都只有一個家庭地址。從HomeAddress到Person的關系可以是1對多,因為可能多個person住在同一個HomeAddress
- 提取屬性(fetched property):提取屬性是關系的備選方法。用提取屬性可以創建一個可在提取時被評估的查詢,從而確定哪些對象屬于這個關系。沿用剛才的例子,一個Person對象可以擁有一個名為Neighbors的提取屬性,該屬性查找數據存儲中與這個Person的HomeAddress擁有相同郵政編碼的所有HomeAddress對象。由于提取屬性的結構和使用方式,它們通常是1對1的關系。提取屬性也是唯一一種能讓你跨越多個數據存儲的關系。
通常,特性,關系和提取屬性都是使用Xcode的數據模型編輯器定義的.
鍵值編碼
我們的代碼中不再使用存取方法和修改方法,而是使用鍵值編碼來設置屬性或檢索它們的已有值。
在操作托管對象時,用于設置和檢索屬性值得鍵就是希望設置的特性的名稱。因此要從托管對象中檢索在name特性中的值,需要調用以下方法:
NSString *name = [myManagedObject valueForKey:@"name"];
同樣,要為托管對象的屬性設置新值,可以執行一下操作:
[myManagedObject setValue:@"daf" forKey:@"dajif"];
在上下文中結合它們
那么,托管對象的活動區域在哪里呢?它們位于所謂的持久存儲中,有時也成為支持存儲(backing store)。持久存儲可以采用多種不同的形式。默認情況下,Core Data應用將支持存儲實現為在應用Documents目錄中的SQLite數據庫。雖然數據是通過SQLite存儲的,但Core Data框架中的類將完成與加載和保存數據相關的所有工作。如果使用Core Data,則不需要編寫任何SQL語句。你只需要操作對象,而內部的工作就由Core Data完成。
除了SQLite之外,支持存儲還可以作為二進制文件實現,甚至以XML格式存儲。還有一種選擇是創建一個內存庫,編寫緩存機制時可以采取這種方法,但它在當前回話結束后無法保存數據。在幾乎所有情況下,你都應該采用默認的設置,并使用SQLite作為持久存儲。
雖然大多數應用都只有一個持久存儲,但也可以在同一應用中使用多個持久存儲。
除了創建它之外(通常在應用委托中實現),我們通常不會直接操作持久存儲,而是使用所謂的托管對象上下文(context)。上下文協調對持久存儲的訪問,同時保存自上次保存對象以來修改過的屬性的信息。上下文還能通過撤銷管理器來取消所有更改,這意味著你可以撤銷單個操作或是回滾到上次保存的數據。(有點類似git哦)
可以將多個上下文指向相同的持久存儲,但大多數ios應用只使用一個。
許多核心數據調用都需要NSManagedObjectContext作為參數,或者需要在上下文中執行。除了一些更加復雜、多線程的iOS應用外,應用委托都可以只使用managedObjectContext屬性--它是Xcode項目魔板自動為應用創建的默認上下文。
你可能會發現,除了委托對象上下文和持久存儲協調者之外,所提供的應用委托還包含一個NSManagedObjectModel實例。該類負責在運行時加載和表示使用Xcode中的數據模型編輯器創建的數據模型。通常,你不需要直接與該類交互。該類由其他Core Data類在后臺使用,因此它們可以確定數據模型中定義了哪些實體和屬性。只要使用所提供的文件創建數據模型,就完全不需要擔心這個類?!?/p>
創建托管對象
創建托管對象的新實例非常簡單,但沒有alloc和init使用起來簡單。這里使用NSEntityDescription類中的insertNewObjectForEntityForName:inManagedObjectContext:工廠方法。NSEntityDescription的工作是跟蹤在應用的數據模型中定義的所有實體并能夠讓你創建這些實體的實例。此方法創建并返回一個實例,表示內存中的單個實體。它返回使用該特定實體的正常屬性設置的NSManagedObject實例,或者如果將實體配置為使用NSManagedObject的特定子類實現,則返回該類的實例。請記住,實體類似于類。實體是對象的描述,用于定義特定的實體具有哪些屬性。
創建新對象的方法如下:
NSManagedObject *thing = [NSEntityDescription insertNewObjectForEntityForName:@"Thing" inManagedObjectContext:context];
這個方法的名稱為insertNewObjectForEntityForName:inManagedObjectContext:,因為除了創建新對象外,它還將此新對象插入到上下文,并返回這個對象。調用結束后,對象存在于上下文中,但還不是持久存儲的一部分。下一次托管對象上下文的save:方法被調用時,這個對象將被添加到持久存儲。
獲取托管對象
要從持久存儲中獲取托管對象,可以使用獲取請求(fetch request),這是Core Data處理預定義的查詢的方式。例如,可以要求“返回所有eyeColor為藍色的Person"
首次創建獲取請求后,為它提供一個NSEntityDescription,指定希望檢索的一個或多個對象實體。下面是一個創建獲取請求的例子:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entityDescr = [NSEntityDescription entityForName:@"Thing" inManagedObjectContext:context];
[request setEntity:entityDescr];
也可以使用NSPredicate類為獲取請求指定條件。Predicate類似于SQL的WHERE子句,可定義條件讓獲取請求得出結果。下面是一個簡單的示例:
NSPredicate *pre = [NSPredicate predicateWithFormat:@"(name = %@)", nameThing];
[request setPredicate:pre];
第一行代碼創建的predicate告訴獲取請求,無需獲取指定實體的所有托管對象,它應僅獲取那些name屬性被設置為當前存儲在nameThinng變量中的值的托管對象。所以,如果nameThing是一個包含值@"Bob"的NSString,則會告訴獲取請求僅返回其name屬性為"Bob"的托管對象。這是一個簡單的例子,predicate復雜很多。
創建了獲取請求并為它提供實體描述后(可以選擇為它指定一個predicate),使用NSManagedObjectContext中的實例方法來執行獲取請求:
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
//handle error
}
executeFetchRequest:error:將從持久存儲中加載特定對象,并在一個數組中返回它們。如果遇到錯誤,則會獲得一個nil數組,并且你提供的錯誤指針將指向描述特定問題的NSError對象。如果沒有遇到錯誤,則會獲得一個有效的數組,但其中可能沒有任何對象,因為可能沒有任何對象滿足指定標準。此后,context(對它執行了請求)將跟蹤對該數組中返回的托管對象的所有更改。向該上下文發送一條save:信息可保存更改。
Core Data的應用
#import "ViewController.h"
#import "AppDelegate.h"
static NSString * const kLineEntityName = @"Line";
static NSString * const kLineNumberKey = @"lineNumber";
static NSString * const kLineTextKey = @"lineText";
@interface ViewController ()
@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:kLineEntityName];
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(@"There was a error!");
//handle error
}
for (NSManagedObject *oneObject in objects) {
int lineNum = [[oneObject valueForKey:kLineNumberKey] intValue];
NSString *lineText = [oneObject valueForKey:kLineTextKey];
UITextField *theField = self.lineFields[lineNum];
theField.text = lineText;
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)applicationWillResignActive:(NSNotification *)notification{
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = appDelegate.managedObjectContext;
NSError *error;
for (int i = 0; i < 4; i++) {
UITextField *textFiled = self.lineFields[i];
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:kLineEntityName];
NSPredicate *pred = [NSPredicate predicateWithFormat:@"(%s = %d)", kLineNumberKey, i];
request.predicate = pred;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(@"There is an error!");
//handle error
}
NSManagedObject *theLine = nil;
if (objects.count > 0) {
theLine = objects[i];
}else {
theLine = [NSEntityDescription insertNewObjectForEntityForName:kLineEntityName inManagedObjectContext:context];
}
[theLine setValue:[NSNumber numberWithInt:i] forKey:kLineNumberKey];
[theLine setValue:textFiled.text forKey:kLineTextKey];
}
[appDelegate saveContext];
}
來看一下viewDidLoad方法。我們需要確定持久存儲中是否已經存在數據,如果有則加載數據并使用它填充字段。該方法首先獲取對應委托的引用,我們將使用這個引用獲得為我們創建的托管對象上下文。
下一個步驟是創建一個獲取請求并將實體描述傳遞給它,以便請求知道要檢索的對象類型
由于我們希望檢索持久存儲中的所有line對象,因此沒有創建predicate.通過執行沒有predicate的請求,上下文將返回庫中的每一個line對象。確保返回的是有效的數組,如果不是則記錄對應的日志。
接下來,我們使用快速枚舉遍歷已獲取托管對象的數組,從中提取每個托管對象的lineNumber和lineText值,并使用該信息更新用戶界面上的一個文本框。
然后,我們需要在應用即將終止(無論是轉到后臺還是完全退出)的時候獲取通知,以便能夠保存用戶對數據作出的任何更改。
我們接下來討論applicationWillResignActive:。這里使用的方法和前面一樣,先獲取對應用委托的引用,然后使用此引用獲取應用的默認上下文指針。
然后使用循環語句為每個標簽執行一次,獲得每個字段對應的索引。
接下來,為line實體創建獲取請求。需要確認持久存儲中是否已經有一個與這個字段對應的托管對象,因此創建一個predicate,用于為字段標識正確的對象。
此時在上下文中執行獲取請求并且檢查objects是否為nil.如果為nil,則表示遇到錯誤,我們應該為應用執行合適的錯誤處理。
現在聲明一個指向NSManagedObject的指針并將它設為nil.執行此操作的原因是,我們還不知道是要從持久存儲里加載對象還是創建新的托管對象。因此,可以檢查與條件匹配的返回對象。如果返回了有效對象,就加載,否則就創建一個新的托管對象來保存這個字段的文本。
接著,使用鍵值編碼來設置行號和此托管對象的文本。
完成循環后,通知context保存更改。