iOS 數據持久化基礎知識小總結

1. 應用的沙盒

注意:如果要為應用程序啟用分享功能,需要打開它的info.plist文件并添加鍵為Application supports iTunes file sharing值為YES的Item;

  • Documents:應用程序可以將數據存儲在Documents目錄中。如果應用程序啟用了iTunes文件分享功能,用戶就可以在iTunes中看到目錄的內容(以及應用程序創建的所有子目錄),還可以對其更新文件。

  • Library:應用程序也可以在這里存儲數據。它用來存放不想分享給用戶的文件。需要時你可以創建自己的子目錄。

  • tmp: tmp目錄供應用存儲臨時文件。當iOS設備執行同步時,iTunes不會備份tmp中的文件。在不需要這些文件時,應用要負責刪除tmp中的文件,以免占用文件系統的控件。

1. 獲取Documents目錄:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentsDirectory = [paths firstObject];

a.常量NSDocumentDirectory表明正在查找Document目錄的路徑。

b.常量NSUserDomainMask表明希望將所搜限制在應用的沙盒中。

NSString *filename = [documentsDirectory stringByAppendingPathComponent:@"theFile.txt"];

完成此調用后,filename就包含了指向用用Documents目錄中theFile.txt文件的完整路徑。然后可以根據filename來創建、讀取和寫入文件。

2. 獲取Library目錄:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);

NSString *libraryDirectory = [paths firstObject];

文件操作同上;

3. 獲取tmp目錄:

NSString *tempPath = NSTemporaryDirectory();

創建該路徑下的文件的路徑:

NSString *tempFile = [tempPath stringByAppendingPathComponent:@"tempFile.txt"];


2. 屬性列表序列化

序列化對象是指可以被轉化為字節流以便于存儲到文件中或者通過網絡進行傳輸的對象。雖然任何對象都可以被序列化,但只有某些對象才能被放置到某個集合類中(如NSDictionary或NSArray中),然后才使用該集合類的writeToFile:atomically:或writeToURL:atomically:方法將它們存儲到屬性列表中。可以按照該方法序列化下面的類:

  • NSArray
  • NSMutableArray
  • NSDictionary
  • NSData
  • NSMutableData
  • NSString
  • NSMutableString
  • NSNumber
  • NSDate
  • 如果只使用這些對象構建數據模型,就可以使用屬性列表來方便的保存和加載數據。

  • 如果打算使用屬性列表持久保存應用數據,則可以使用字典或數組。假設放到數組或者字典中的所有對象都是前面列出的可序列化對象,則可以通過對字典或者數組實例調用writeToFile:atomically:方法來寫入屬性列表,如下:

[myArray writeToFile:@"some/file/location/output.plist" atomically:YES];

注意:atomically參數讓該方法將數據寫入輔助文件,而不是寫入指定位置。成功寫入該文件后,輔助文件將被復制到第一個參數指定的位置。這是更安全的寫入文件的方法,因為如果應用在保存期間崩潰,則現有文件不會被破壞。盡管這增加了一點開銷,但是多數情況下還是值得的。

  • 屬性列表方法的一個問題是無法將自定義對象序列化到屬性列表中,另外也不能使用沒有在可以序列化對象類型列表中指定的Cocoa Touch的其他類。這意味著無法直接使用NSURL、UIImage和UIColor等類。

  • 且不說序列化問題,將這些模型對象保存到屬性列表中還意味著你無法輕松創建派生的或需要計算的屬性(例如,等于兩個屬性之和的屬性),并且必須將實際上應該包含在模型類中的某些代碼移動到控制器類。這些限制也適用于簡單的數據模型和簡單應用。但在多數情況下,如果創建了專用的模型類,則應用更容易維護。

  • 在復雜的應用中,簡單的屬性列表仍然非常有用。它們是將靜態數據包含在應用中的最佳方法。例如,當應用包含一個選取器時,創建一個屬性列表文件并將其放在項目的Resources文件夾中,就是將項目列表包含到選取器中的最佳方法,這樣就能把項目列表編譯到應用中。



3.對模型對象進行歸檔

  • 在Cocoa世界中,歸檔是指另一種形式的序列化,但它是任何對象都可以實現的更常規的類型。專門編寫用于保存數據的任何模型對象都應該支持歸檔。使用對模型對象進行歸檔的技術可以輕松將復雜的對象寫入文件,然后讀取它們。
  • 只要在類中實現的每個屬性都是標量(如整型和浮點型)或都是遵循NSCoding協議的某個類的實例,你就可以將整個對象進行完全的歸檔。由于大多數支持存儲數據的Foundation和Cocoa Touch類都遵循NSCoding協議(不過有一些例外,如UIImage),對于大多數類來說,歸檔相對而言比較容易實現。
  • 盡管對歸檔的使用沒有嚴格要求,但還有一個協議應該與NSCoding一起實現,即NSCopying協議。后者允許賦值對象,這使在使用數據模型對象時具備了較大的靈活性。

1.遵循NSCoding協議

NSCoding協議聲明了兩個方法,這兩個方法都是必需的。一個方法將對象編碼到歸檔中,另一個方法對歸檔解碼來創建一個新對象。這兩個方法都傳遞一個NSCoder實例,使用方式與NSUserDefaults非常相似。也可以使用KVC對對象和原生數據模型(如整型和浮點型)進行編碼和解碼。

  • 在OC中,必須使用正確的編碼方法將所有實例變量編碼為encoder。如果要子類化某個也遵循NSCoding的類,還需要確保對其父類調用encodeWithCoder:方法:
- (void)encodeWithCoder:(NSCoder *)encoder {
  [super encodeWithCoder:encoder]; // 讓父類對狀態進行編碼
  [encoder encodeObject:foo forKey:kFooKey];
  [encoder encodeObject:bar forKey:kBarKey];
  [encoder encodeInt:someInt forKey:kSomeIntKey];
  [encoder encodeFloat:someFloat forKey:kSomeFloatKey];
}
  • 還需要實現一個通過NSCoder初始化對象的初始化方法,恢復之前歸檔的對象。
  • OC中實現initWithCoder:方法比實現encodeWithCoder:方法稍微復雜一些。如果直接對NSObject進行子類化,或者對某些不遵循NSCoding的其他類進行子類化,如下:
- (id)initWithCoder:(NSCoder *)decoder {

  if (self = [super init]) {

    foo = [decoder decodeObjectForKey:kFooKey];
    bar = [decoder decodeObjectForKey:kBarKey];
    someInt = [decoder decodeIntForKey:kSomeIntKey];
    someFloat = [decoder decodeFloatForKey:kSomeFloatKey];
  }
  return self;
}
  • 該方法使用[super init]初始化對象實例。如果初始化成功,則它通過解碼NSCoder的實例中傳遞的值來設置其屬性。當為某個具有父類遵循NSCoding的類實現NSCoding時,initWithCoder:方法稍有不同。它不再對super調用init,而是調用initWithCoder:,如:
- (id)initWithCoder:(NSCoder *)decoder {

  if (self = [super initWithCoder:decoder]) {

    foo = [decoder decodeObjectForKey:kFooKey];
    bar = [decoder decodeObjectForKey:kBarKey];
    someInt = [decoder decodeIntForKey:kSomeIntKey];
    someFloat = [decoder decodeFloatForKey:kSomeFloatKey];
  }
  return self;
}

2. 實現NSCopying協議

  • NSCopying有一個copyWithZone方法,可用來復制對象。實現NSCopying與實現initWithCoder非常相似,只需創建一個同一類的新實例,然后將實例的所有屬性都設置為與該對象屬性相同的值即可。
- (id) copyWithZone:(NSZone *)zone {

  MyClass *copy = [[[self class] allocWithZone:zone] init];
  copy.foo = [self.foo copyWithZone:zone];
  copy.bar = [self.bar copyWithZone:zone];
  copy.someInt = self.someInt;
  copy.someFloat = self.someFloat;

  return copy;
}
  • 不要過于擔心NSZone參數。它指向系統用于管理內存的struct。只有在極少數情況下,才需要關注zone或者自己創建的zone。對某個對象調用copy的方法與使用默認zone調用copyWithZone的方法完全相同,幾乎始終能滿足需求。事實上,現在的iOS完全可以忽略zone。NSCopying用到zone在本質上是考慮向后兼容性所致。

3. 對數據對象進行歸檔和取消歸檔

  • 從遵循NSCoding的一個或者多個對象創建歸檔相對比較容易。首先創建一個NSMutableData實例,用于包含編碼的數據,然后創建一個NSKeyedArchiver實例,用于將對象歸檔到此NSMutableData實例中:
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
  • 創建這兩個實例后,使用key/value來對希望包含在歸檔中的所有對象進行歸檔:
[archiver encodeObject:myObject forKey:@"keyValueString"];
  • 對所有要包含的對象進行編碼之后,只需告知歸檔程序已經完成了這些操作,并將NSMutableData實例寫入文件系統:
[archiver finishEncoding];
BOOL success = [data writeToFile:@"/path/to/archive" atomically:YES];
  • 寫入文件時出現錯誤會將success設置為false或NO。如果success為true或YES,則數據已成功寫入指定文件。從該歸檔創建的任何對象與之前寫入該文件的對象一致。
  • 有一個快速方法可以獲取同樣的內容:使用NSKeyedArchiver的archiveDataWithRootObject方法可以分配一個NSData對象并一次性將對象編碼進去,然后返回NSData對象。
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];
BOOL success = [data writeToFile:@"path/to/archive" atomically:YES];
  • 也可以使用archiveRootObject:toFile:方法直接從對象歸檔數據到文件:
BOOL success = [NSKeyedArchiver archiveRootObject:object toFile:@"/path/to/archive"];
  • 從歸檔重組對象的步驟與上面相似。從歸檔文件創建一個NSData實例,并創建一個NSKeyedUnarchiver對數據進行解碼:
NSData *data = [[NSData alloc] initWithContentsOfFile:@"/path/to/archive"];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
  • 然后,使用之前對對象進行歸檔的同一個key從解壓程序中讀取對象:
self.object = [unarchiver decodeObjectForKey:@"keyValueString"];
  • 最后,告知歸檔程序已經完成了該操作:
[unarchiver finishDecoding];


4. 使用iOS內嵌的SQLite3

1. 創建或打開數據庫

使用SQLite3之前,必須打開數據庫。用于執行此操作的命令是sqlite3_open,這樣將打開一個現有數據庫。如果指定位置上不存在數據庫,則函數會創建一個新的數據庫。下面是打開數據庫的代碼:

sqlite3 *database;
int result = sqlite3_open("/path/to/database/file", &database);

如果result等于常量SQLITE_OK,就表示數據庫已成功打開。需要注意的是,數據庫文件的路徑必須以C字符串的形式進行傳遞。SQLite3是采用可移植的C(而非OC)編寫的,它不知道什么是NSString。所幸,有一個NSString方法能從NSString實例生成C字符串:

const char *stringPath = [pathString 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);

提示: 在OC中,如果兩個字符串之間除了空白(包括換行符)之外沒有其他的分隔字符,那么這兩個字符串會被連接為一個字符串。

如之前所做的一樣,需要檢查result是否等于SQLITE_OK以確保命令成功運行。如果命令未成功運行,errorMsg或errMsg將對所發生的問題進行描述。

函數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);

注意: OC中所有接受字符串的SQLite3函數都要求使用舊樣式的C字符串。在實例中,可以創建并傳遞一個C字符串,也可以創建一個NSString并通過它的方法(名為UTF8String)派生一個C字符串。這兩個方法都行。如果需要操作字符串,則使用NSString或NSMutableString比較容易,但將NSString轉換為C字符串會導致一些額外開銷。

如果result等于SQLITE_OK,則語句準備成功,可以開始遍歷結果集。

遍歷結果集并從數據庫中檢索整型和字符串:

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];
  // 這里寫對數據進行處理的代碼
}
sqlite3_finalize(statement);

2. 綁定變量

雖然可以通過創建SQL字符串來插入值,但常用的方法是使用綁定變量來執行數據庫插入操作。正確處理字符串并確保它們沒有無效字符(以及引號處理過的屬性)是非常繁瑣的事情。借助綁定變量,這些問題迎刃而解。

要使用綁定變量插入值,只需按正常方式創建SQL語句即可,不過要在SQL字符串中添加一個問號。每個問號都表示一個需要在語句執行之前進行綁定的變量。然后,準備好SQL語句,將值綁定到各個變量并執行命令。

下面這個實例使用兩個綁定變量預處理SQL語句。它將整形數綁定到第一個變量,將字符串綁定到第二個變量,然后執行結束語句:

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來代替字符串的長度,這樣函數將使用整個字符串。對于所有其他情況,需要指定所傳遞數據的長度。

  • 另一個參數是可選的函數回調,用于在語句執行后完成內存清理工作。通常,這種函數使用malloc()釋放已分配的內存。

綁定語句后面的語法看起來可能有點奇怪,因為我們執行了一個插入操作。當使用綁定變量時,會將相同語法同時用于查詢和更新。如果SQL字符串包含一個SQL查詢(而不是更新),就需要多次調用sqlite3_step(),直到它返回SQLITE_DONE。疑問這里是更新,所以僅調用一次。

3. SQLite3 的應用

1. 鏈接到SQLite3庫

通過一個過程API來訪問SQLite3,該API提供對很多C函數調用的接口。要使用該API,需要將應用鏈接到一個名為libsqlite3.tbd的動態庫。

2. 實現代碼
#import "ViewController.h"
#import <sqlite3.h>

@interface ViewController ()

@property (nonatomic, strong)IBOutletCollection(UITextField) NSArray *lineFields; // 四個UITextField

@end

@implementation ViewController

- (NSString *)dataFilePath {
    
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    return [documentsDirectory stringByAppendingString:@"data.sqlite"];
}

- (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 PRIMARK KEY, FIELD_DATA TEXT);";
    char *errorMsg;
    if (sqlite3_exec(database, [createSQL UTF8String], NULL, NULL, &errorMsg)) {
        sqlite3_close(database);
        NSAssert(0, @"Error creating table: %s", errorMsg);
    }
    
    NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
    sqlite3_stmt *statement;
    if (sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil) == SQLITE_OK) {
        while (sqlite3_step(statement) == SQLITE_ROW) {
            int row = sqlite3_column_int(statement, 0);
            char *rowData = (char *)sqlite3_column_text(statement, 1);
            NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
            UITextField *field = self.lineFields[row];
            field.text = fieldValue;
        }
        sqlite3_finalize(statement);
    }
    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);
}

@end

首先我們打開數據庫,如果打開時遇到了問題,則關閉它并拋出一個斷言錯誤(或者打印一段錯誤信息并中斷程序):

sqlite3 *database;
    if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
        
        sqlite3_close(database);
        NSAssert(0, @"Failed to open database");
    }

接下來,需要確保有一個表來保存我們的數據。可以使用CREATE TABLE完成此任務。通過指定IF NOT EXISTS,可以防止數據庫覆蓋現有數據。如果已有一個具有相同名稱的表,此命令不會執行任何操作,所以可以在應用每次啟動時安全的調用它,不需要顯式的檢查表是否存在:

 NSString *createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS"
                        "(ROW INTEGER PRIMARK KEY, FIELD_DATA TEXT);";
    char *errorMsg;
    if (sqlite3_exec(database, [createSQL UTF8String], NULL, NULL, &errorMsg)) {
        sqlite3_close(database);
        NSAssert(0, @"Error creating table: %s", errorMsg);
    }

數據庫中的每一行包含一個整型和一個字符串。整型指出圖形界面中得到的是哪一行的數字(從0開始計數)。而字符串是這一行中文本框的內容。最后,需要加載數據。為此,使用SELECT語句。在這個例子中,創建一個SELECT從數據庫請求所有行并要求SQLite3準備的SELECT。要告訴SQLite3按行號順序排序各行,以便總是以相同順序獲取它們。否則,SQLite3將按內部存儲數據返回各行:

    NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
    sqlite3_stmt *statement;
    if (sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil) == SQLITE_OK) {

然后遍歷返回的每一行:

while (sqlite3_step(statement) == SQLITE_ROW) {

抓取行號并將它存儲在一個int變量中,然后抓取字段數據保存到C語言字符串中:

int row = sqlite3_column_int(statement, 0);
char *rowData = (char *)sqlite3_column_text(statement, 1);

接下來,利用從數據庫獲取的值設置相應的字段:

NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
UITextField *field = self.lineFields[row];
field.text = fieldValue;

最后關閉數據庫連接,所有操作到此結束:

         }
            sqlite3_finalize(statement);
     }
    sqlite3_close(database);

注意,示例在創建表和加載它所包含的所有數據后立即關閉了數據庫連接,而不是在應用運行的整個過程中保持打開狀態。這是管理連接最簡單的方式,對于這個小示例,可以在需要連接時再打開它。在其他需要頻繁使用數據庫的應用中,可能有必要始終打開連接。

其他更改是在applicationWillResignActive方法中進行的,我們需要把應用數據保存在這里。

applicationWillResignActive方法首次會再次打開數據庫。然后保存數據,在4個字段中進行循環,生成4條獨立的命令來更新數據庫中的每一行:

for (int i = 0; i < 4; i++) {
        UITextField *field = self.lineFields[i];

示例設計了一條帶有兩個綁定變量的INSERT OR REPLACE語句。第一個變量代表所存儲的行,第二個變量代表要存儲的實際字符串值。使用INSERT OR REPLACE,而不是更標準的INSERT,就不需要擔心某個行是否已經存在:

        char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA)"
        "VALUES (?, ?);";

接下來聲明一個指向語句的指針,然后為語句添加綁定變量,并將值綁定到兩個綁定變量:

       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);
       }

然后調用sqlite3_step來執行更新,檢查并確定其運行正常,然后完成語句,結束循環:

          if (sqlite3_step(stmt) != SQLITE_DONE) {
            NSAssert(0, @"Error updating table: %s", errorMsg);
        }
        sqlite3_finalize(stmt);

注意,OC代碼中使用了一個斷言來檢查錯誤條件。之所以會使用斷言,而不使用異常或手動錯誤檢查,是因為這種情況只有在開發人員出錯的情況下才會出現。使用此斷言宏將有助于我們調試代碼,并且可以脫離最終的應用。

注意: 有一個條件可能導致前面的SQLite代碼出現錯誤,但不是程序員錯誤。如果設備的存儲區已滿,SQLite無法將其更改保存到數據庫,那么這里也會發生錯誤。但是,這種情況很少見,并可能為用戶帶來更深層次的問題,不過這已經超出了應用數據的范圍。如果系統處于這一狀態,我們的應用甚至可能無法成功啟動。因此可以不用考慮這個問題。

完成循環之后,關閉數據庫:

sqlite3_close(database);
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容