iOS應用資料儲存的幾種常用方式

小莊的風景發表於2017-02-12

應用沙盒

1)每個iOS應用都有自己的應用沙盒(應用沙盒就是檔案系統目錄),與其他檔案系統隔離。應用必須待在自己的沙盒裡,其他應用不能訪問該沙盒

2)應用沙盒的檔案系統目錄,如下圖所示(假設應用的名稱叫Layer)

1353118-9887ba61908edade.jpg

應用沙盒的檔案系統目錄

3)應用沙盒結構分析

  • 應用程式包:(上圖中的Layer)包含了所有的資原始檔和可執行檔案

  • Documents:儲存應用執行時生成的需要持久化的資料,iTunes同步裝置時會備份該目錄。例如,遊戲應用可將遊戲存檔儲存在該目錄

  • tmp:儲存應用執行時所需的臨時資料,使用完畢後再將相應的檔案從該目錄刪除。應用沒有執行時,系統也可能會清除該目錄下的檔案。iTunes同步裝置時不會備份該目錄

  • Library/Caches:儲存應用執行時生成的需要持久化的資料,iTunes同步裝置時不會備份該目錄。一般儲存體積大、不需要備份的非重要資料

  • Library/Preference:儲存應用的所有偏好設定,iOS的Settings(設定)應用會在該目錄中查詢應用的設定資訊。iTunes同步裝置時會備份該目錄

4)應用沙盒目錄的常見獲取方式

  • 沙盒根目錄:NSString *home = NSHomeDirectory();

  • Documents:(2種方式):

i)利用沙盒根目錄拼接”Documents”字串

1
2
3
NSString *home = NSHomeDirectory();
NSString *documents = [home stringByAppendingPathComponent:@"Documents"];
// 不建議採用,因為新版本的作業系統可能會修改目錄名

ii)利用NSSearchPathForDirectoriesInDomains函式

1
2
3
4
5
// NSUserDomainMask 代表從使用者資料夾下找
// YES 代表展開路徑中的波浪字元“~”
NSArray *array =  NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
// 在iOS中,只有一個目錄跟傳入的引數匹配,所以這個集合裡面只有一個元素
NSString *documents = [array objectAtIndex:0];
  • tmp:NSString *tmp = NSTemporaryDirectory();

  • Library/Caches:(跟Documents類似的2種方法)

i)利用沙盒根目錄拼接”Caches”字串

ii)利用NSSearchPathForDirectoriesInDomains函式(將函式的第2個引數改為:NSCachesDirectory即可)

  • Library/Preference:通過NSUserDefaults類存取該目錄下的設定資訊

iOS應用資料儲存的常用方式

  • XML屬性列表(plist)歸檔

  • Preference(偏好設定)

  • NSKeyedArchiver歸檔(NSCoding)

  • SQLite3

  • Core Data

XML屬性列表(plist)歸檔

屬性列表是一種XML格式的檔案,擴充名為plist。

如果物件是NSString、NSDictionary、NSArray、NSData、NSNumber等型別,就可以使用writeToFile:atomically:方法直接將物件寫到屬性列表檔案中。

舉個例子:將一個NSDictionary物件歸檔到一個plist屬性列表中

1
2
3
4
5
6
7
// 將資料封裝成字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@"母雞" forKey:@"name"];
[dict setObject:@"15013141314" forKey:@"phone"];
[dict setObject:@"27" forKey:@"age"];
// 將字典持久化到Documents/stu.plist檔案中
[dict writeToFile:path atomically:YES];

成功寫入到Documents目錄下:

1461840164876699.png

執行結果

用文字編輯器開啟,檔案內容為:

1353118-b741943dd4a08d67.jpg

文字編輯器檢視

用xcode開啟屬性檔案:

1353118-79356fa27c45eed4.jpg

xcode檢視

讀取屬性列表,恢復NSDictionary物件

1
2
3
4
5
// 讀取Documents/stu.plist的內容,例項化NSDictionary
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSLog(@"name:%@", [dict objectForKey:@"name"]);
NSLog(@"phone:%@", [dict objectForKey:@"phone"]);
NSLog(@"age:%@", [dict objectForKey:@"age"]);

1353118-b716578072700e39.jpg

輸出結果:

1353118-dc978e8864a6891f.jpg

屬性列表-NSDictionary的儲存和讀取過程

Preference(偏好設定)

很多iOS應用都支援偏好設定,比如儲存使用者名稱、密碼、字型大小等設定,iOS提供了一套標準的解決方案來為應用加入偏好設定功能。

每個應用都有個NSUserDefaults例項,通過它來存取偏好設定。

比如,儲存使用者名稱、字型大小、是否自動登入

1
2
3
4
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"itcast" forKey:@"username"];
[defaults setFloat:18.0f forKey:@"text_size"];
[defaults setBool:YES forKey:@"auto_login"];

1353118-55bb258a3d05246d.jpg

儲存檔案內容

NSKeyedArchiver歸檔(NSCoding)

如果物件是NSString、NSDictionary、NSArray、NSData、NSNumber等型別,可以直接用NSKeyedArchiver進行歸檔和恢復。

不是所有的物件都可以直接用這種方法進行歸檔,只有遵守了NSCoding協議的物件才可以。

NSCoding協議有2個方法:

  • encodeWithCoder:

每次歸檔物件時,都會呼叫這個方法。一般在這個方法裡面指定如何歸檔物件中的每個例項變數,可以使用encodeObject:forKey:方法歸檔例項變數

  • initWithCoder:

每次從檔案中恢復(解碼)物件時,都會呼叫這個方法。一般在這個方法裡面指定如何解碼檔案中的資料為物件的例項變數,可以使用decodeObject:forKey方法解碼例項變數

歸檔一個NSArray物件到Documents/array.archive

1
2
NSArray *array = [NSArray arrayWithObjects:@”a”,@”b”,nil];
[NSKeyedArchiver archiveRootObject:array toFile:path];

歸檔成功

1353118-d2f19a707d722c16.jpg

存檔檔案

恢復(解碼)NSArray物件

1
NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

1353118-391ffe7d0eca524e.jpg

存取過程

歸檔Person物件

Person.h

1
2
3
4
@interface Person : NSObject@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) float height;
@end

Person.m

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation Person
- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeInt:self.age forKey:@"age"];
    [encoder encodeFloat:self.height forKey:@"height"];
}
- (id)initWithCoder:(NSCoder *)decoder {
    self.name = [decoder decodeObjectForKey:@"name"];
    self.age = [decoder decodeIntForKey:@"age"];
    self.height = [decoder decodeFloatForKey:@"height"];
    return self;
}
@end

歸檔(編碼)

1
2
3
4
5
Person *person = [[[Person alloc] init] autorelease];
person.name = @"hosea";
person.age = 22;
person.height = 1.83f;
[NSKeyedArchiver archiveRootObject:person toFile:path];

恢復(解碼)

1
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

NSKeyedArchiver-歸檔物件的注意

1
2
3
4
5
6
7
- 如果父類也遵守了NSCoding協議,請注意:
    - 應該在encodeWithCoder:方法中加上一句
    [super encodeWithCode:encode];
    確保繼承的例項變數也能被編碼,即也能被歸檔
    - 應該在initWithCoder:方法中加上一句
    self = [super initWithCoder:decoder];
    確保繼承的例項變數也能被解碼,即也能被恢復

歸檔NSData

使用archiveRootObject:toFile:方法可以將一個物件直接寫入到一個檔案中,但有時候可能想將多個物件寫入到同一個檔案中,那麼就要使用NSData來進行歸檔物件。

NSData可以為一些資料提供臨時儲存空間,以便隨後寫入檔案,或者存放從磁碟讀取的檔案內容。可以使用[NSMutableData data]建立可變資料空間。

1353118-4ed13bedff51f5d4.jpg

原理

舉個例子:NSData-歸檔2個Person物件到同一檔案中

歸檔(編碼)

1
2
3
4
5
6
7
8
9
10
11
// 新建一塊可變資料區
NSMutableData *data = [NSMutableData data];
// 將資料區連線到一個NSKeyedArchiver物件
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
// 開始存檔物件,存檔的資料都會儲存到NSMutableData中
[archiver encodeObject:person1 forKey:@"person1"];
[archiver encodeObject:person2 forKey:@"person2"];
// 存檔完畢(一定要呼叫這個方法,呼叫了這個方法,archiver才會將encode的資料儲存到NSMutableData中)
[archiver finishEncoding];
// 將存檔的資料寫入檔案
[data writeToFile:path atomically:YES];

恢復(解碼)

1
2
3
4
5
6
7
8
// 從檔案中讀取資料
NSData *data = [NSData dataWithContentsOfFile:path];
// 根據資料,解析成一個NSKeyedUnarchiver物件
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Person *person1 = [unarchiver decodeObjectForKey:@"person1"];
Person *person2 = [unarchiver decodeObjectForKey:@"person2"];
// 恢復完畢(這個方法呼叫之後,unarchiver不能再decode物件,而且會通知unarchiver的代理呼叫unarchiverWillFinish:和unarchiverDidFinish:方法)
[unarchiver finishDecoding];

PS:也可將多個物件放入到一個陣列中。

  • 將陣列進行歸檔,在陣列物件執行archiveRootObject:toFile時,陣列中每個物件會自動呼叫encodeWithCoder:方法進行歸檔;

  • 相反陣列檔案進行解檔時,在陣列物件執行unarchiveObjectWithFile:時,陣列中每個物件會自動呼叫initWithCoder:方法進行解檔。

利用歸檔實現深複製

比如對一個Person物件進行深複製

1
2
3
4
5
6
7
// 臨時儲存person1的資料
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person1];
// 解析data,生成一個新的Person物件
Person *person2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
// 分別列印記憶體地址
NSLog(@"person1:0x%x", person1); // person1:0x7177a60
NSLog(@"person2:0x%x", person2); // person2:0x7177cf0

1353118-ffc98002136a4b44.jpg

深複製原理

SQLite3

SQLite3簡介

SQLite3是一款開源的嵌入式關係型資料庫,可移植性好、易使用、記憶體開銷小。

SQLite3是無型別的,意味著你可以儲存任何型別的資料到任意表的任意欄位中。比如下列的創表語句是合法的:

1
create table t_person(name, age);

為了保證可讀性,建議還是把欄位型別加上:

1
create table t_person(name text, age integer);

SQLite3常用的5種資料型別:text、integer、float、boolean、blob

在iOS中使用SQLite3,首先要新增庫檔案libsqlite3.dylib和匯入主標頭檔案

1353118-d7cd70cf7fca9851.jpg

匯入庫

建立或開啟資料庫

1
2
3
// path為:~/Documents/person.db
sqlite3 *db;
int result = sqlite3_open([path UTF8String], &db);

程式碼解析:

  • sqlite3_open()將根據檔案路徑開啟資料庫,如果不存在,則會建立一個新的資料庫。如果result等於常量SQLITE_OK,則表示成功開啟資料庫

  • sqlite3 *db:一個開啟的資料庫例項

  • 資料庫檔案的路徑必須以C字串(而非NSString)傳入

關閉資料庫:sqlite3_close(db);

執行創表語句

1
2
3
char *errorMsg;  // 用來儲存錯誤資訊
char *sql = "create table if not exists t_person(id integer primary key autoincrement, name text, age integer);";
int result = sqlite3_exec(db, sql, NULL, NULL, &errorMsg);

程式碼解析:

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

  • sqlite3_exec()還可以執行的語句:

  • 開啟事務:begin transaction;

  • 回滾事務:rollback;

  • 提交事務:commit;

帶佔位符插入資料

1
2
3
4
5
6
7
8
9
10
char *sql = "insert into t_person(name, age) values(?, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
    sqlite3_bind_text(stmt, 1, "母雞", -1, NULL);
    sqlite3_bind_int(stmt, 2, 27);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
    NSLog(@"插入資料錯誤");
}
sqlite3_finalize(stmt);

程式碼解析:

  • sqlite3_prepare_v2()返回值等於SQLITE_OK,說明SQL語句已經準備成功,沒有語法問題

  • sqlite3_bind_text():大部分繫結函式都只有3個引數

  • 第1個引數是sqlite3_stmt *型別

  • 第2個引數指佔位符的位置,第一個佔位符的位置是1,不是0

  • 第3個引數指佔位符要繫結的值

  • 第4個引數指在第3個引數中所傳遞資料的長度,對於C字串,可以傳遞-1代替字串的長度

  • 第5個引數是一個可選的函式回撥,一般用於在語句執行後完成記憶體清理工作

  • sqlite_step():執行SQL語句,返回SQLITE_DONE代表成功執行完畢

  • sqlite_finalize():銷燬sqlite3_stmt *物件

查詢資料

1
2
3
4
5
6
7
8
9
10
11
12
char *sql = "select id,name,age from t_person;";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int _id = sqlite3_column_int(stmt, 0);
        char *_name = (char *)sqlite3_column_text(stmt, 1);
        NSString *name = [NSString stringWithUTF8String:_name];
        int _age = sqlite3_column_int(stmt, 2);
        NSLog(@"id=%i, name=%@, age=%i", _id, name, _age);
    }
}
sqlite3_finalize(stmt);

程式碼解析

  • sqlite3_step()返回SQLITE_ROW代表遍歷到一條新記錄

  • sqlite3_column_*()用於獲取每個欄位對應的值,第2個引數是欄位的索引,從0開始

Core Data

Core Data簡單介紹

  • Core Data框架提供了物件-關係對映(ORM)的功能,即能夠將OC物件轉化成資料,儲存在SQLite3資料庫檔案中,也能夠將儲存在資料庫中的資料還原成OC物件。在此資料操作期間,不需要編寫任何SQL語句。使用此功能,要新增CoreData.framework和匯入主標頭檔案CoreData/CoreData.h。

1353118-801e5c3548ba5238.jpg

物件-關係對映

  • 在Core Data,需要進行對映的物件稱為實體(entity),而且需要使用Core Data的模型檔案來描述應用的所有實體和實體屬性

這裡以Person和Card(身份證)2個實體為例子,先看看實體屬性和之間的關聯關係

1353118-bcb9d93c189ca107.jpg

實體屬性和之間的關聯關係

  • Person中有個Card屬性,Card中有個Person屬性。

  • 屬於一對一雙向關聯。

模型檔案

建立檔案

1353118-847db2b702ae2142.jpg

新增實體

1353118-8e522e29273d4c03.png

新增Person實體的基本屬性

1353118-3c28a1adca613a50.jpg

新增Card實體的基本屬性

1353118-e5bc2b8fa5ccce37.jpg

在Person中新增card屬性

1353118-816276353724915a.jpg

在Card中新增person屬性

1353118-8bb511e4742b94f7.jpg

NSManagedObject

  • 通過Core Data從資料庫取出的物件,預設情況下都是NSManagedObject物件

  • NSManagedObject的工作模式有點類似於NSDictionary物件,通過鍵-值對來存取所有的實體屬性

  • setValue:forKey: 儲存屬性值(屬性名為key)

  • valueForKey: 獲取屬性值(屬性名為key)

1353118-21be3c3c51f728ac.jpg

Core Data主要物件

1353118-e79a0387a70eb1cb.jpg

搭建Core Data上下文環境

從應用程式包中載入模型檔案

1
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];

傳入模型,初始化NSPersistentStoreCoordinator

1
NSPersistentStoreCoordinator *psc = [[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model] autorelease];

構建SQLite檔案路徑

1
2
NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSURL *url = [NSURL fileURLWithPath:[docs stringByAppendingPathComponent:@"person.data"]];

新增持久化儲存庫,這裡使用SQLite作為儲存庫

1
2
3
4
5
NSError *error = nil;
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error];
if (store == nil) { // 直接拋異常
  [NSException raise:@"新增資料庫錯誤" format:@"%@", [error localizedDescription]];
}

初始化上下文,設定persistentStoreCoordinator屬性

1
2
3
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
context.persistentStoreCoordinator = psc;
// 用完之後,還是要[context release];

新增資料

傳入上下文,建立一個Person實體物件

1
NSManagedObject *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context];

設定簡單屬性

1
2
[person setValue:@"hosea" forKey:@"name"];
[person setValue:[NSNumber numberWithInt:22] forKey:@"age"];

傳入上下文,建立一個Card實體物件

1
2
NSManagedObject *card = [NSEntityDescription insertNewObjectForEntityForName:@"Card" inManagedObjectContext:context];
[card setValue:@"4414241933432" forKey:@"no"];

設定Person和Card之間的關聯關係

1
[person setValue:card forKey:@"card"];

利用上下文物件,將資料同步到持久化儲存庫

1
2
3
4
5
6
NSError *error = nil;
BOOL success = [context save:&error];
if (!success) {
  [NSException raise:@"訪問資料庫錯誤" format:@"%@", [error localizedDescription]];
}
//如果是想做更新操作:只要在更改了實體物件的屬性後呼叫[context save:&error],就能將更改的資料同步到資料庫

查詢資料

初始化一個查詢請求

1
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];

設定要查詢的實體

1
NSEntityDescription *desc = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:context];

設定排序(按照age降序)

1
2
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
request.sortDescriptors = [NSArray arrayWithObject:sort];

設定條件過濾(name like '%hosea-1%')

1
2
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name like %@", @"*hosea-1*"];
request.predicate = predicate;

執行請求

1
2
3
4
5
NSError *error = nil;
NSArray *objs = [context executeFetchRequest:request error:&error];
if (error) {
  [NSException raise:@"查詢錯誤" format:@"%@", [error localizedDescription]];
}

遍歷資料

1
2
3
for (NSManagedObject *obj in objs) {
  NSLog(@"name=%@", [obj valueForKey:@"name"]);
}

刪除資料

傳入需要刪除的實體物件

1
[context deleteObject:managedObject];

將結果同步到資料庫

1
2
3
4
5
NSError *error = nil;
[context save:&error];
if (error) {
  [NSException raise:@"刪除錯誤" format:@"%@", [error localizedDescription]];
}

開啟Core Data的SQL日誌輸出開關

1353118-5d4b45107f5a7280.jpg

Core Data的延遲載入

  • Core Data不會根據實體中的關聯關係立即獲取相應的關聯物件

  • 比如通過Core Data取出Person實體時,並不會立即查詢相關聯的Card實體;當應用真的需要使用Card時,才會查詢資料庫,載入Card實體的資訊

建立NSManagedObject的子類

預設情況下,利用Core Data取出的實體都是NSManagedObject型別的,能夠利用鍵-值對來存取資料

但是一般情況下,實體在存取資料的基礎上,有時還需要新增一些業務方法來完成一些其他任務,那麼就必須建立NSManagedObject的子類

1353118-0ae824488f87d4c7.jpg

選擇模型檔案

1353118-ad48dc961b0dc63b.jpg

選擇需要建立子類的實體

1353118-7d62d41d5e888d25.jpg

1353118-c2e164c1d17cd6ee.jpg

那麼生成一個Person實體物件就應該這樣寫

1
2
3
4
5
6
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context];
person.name = @"hosea";
person.age = [NSNumber numberWithInt:22];
Card *card = [NSEntityDescription insertNewObjectForEntityForName:@”Card" inManagedObjectContext:context];
card.no = @”4414245465656";
person.card = card;

相關文章