持久化,就是將數據保存起來。使得下次APP啟動的時候,可以繼續訪問。
在iOS開發中,我總結了一下,經常用到的幾種數據存數存儲方案。
- plist文件
- 偏好設置(preference)
- 歸解檔(NSKeyedArchiver)
- SQlite 3
- CoreData
沙盒
每個iOS程序都有一個獨立的文件系統(存儲空間),而且只能在對應的文件系統中進行操作,也就是默認情況下只能訪問程序自己的目錄,此區域被稱為沙盒。
在介紹各種存儲方法之前,我們必須了解一下沙盒的基本概念。
沙盒結構
如下:
"應用程序包"
Documents
Library
Caches
Preferences
tmp
順便上傳網上copy的圖片:
沙盒結構
沙盒中相關路徑
-
AppName.app
應用程序的程序包目錄,包含應用程序的本身。由于應用程序必須經過簽名,所以不能在運行時對這個目錄中的內容進行修改,否則會導致應用程序無法啟動。 -
Documents/
保存應用程序的重要數據文件和用戶數據文件等。用戶數據基本上都放在這個位置(例如從網上下載的圖片或音樂文件),該文件夾在應用程序更新時會自動備份,在連接iTunes時也可以自動同步備份其中的數據。 -
Library:
這個目錄下有兩個子目錄,可創建子文件夾。可以用來放置您希望被備份但不希望被用戶看到的數據。該路徑下的文件夾,除Caches以外,都會被iTunes備份.
Library/Caches:
保存應用程序使用時產生的支持文件和緩存文件(保存應用程序再次啟動過程中需要的信息),還有日志文件最好也放在這個目錄。iTunes 同步時不會備份該目錄并且可能被其他工具清理掉其中的數據。
Library/Preferences:
保存應用程序的偏好設置文件。NSUserDefaults類創建的數據和plist文件都放在這里。會被iTunes備份。 - tmp/: 保存應用運行時所需要的臨時數據。不會被iTunes備份。iPhone重啟時,會被清空。
獲取沙盒各個目錄路徑方法:
// 獲取沙盒根目錄路徑
NSString *homeDir = NSHomeDirectory();
// 獲取Documents目錄路徑
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) firstObject];
//獲取Library的目錄路徑
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES) lastObject];
// 獲取cache目錄路徑
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) firstObject];
// 獲取tmp目錄路徑
NSString *tmpDir =NSTemporaryDirectory();
// 獲取應用程序程序包中資源文件路徑的方法:
NSString *bundle = [[NSBundle mainBundle] bundlePath];
NSLog(@"homeDir=%@ \n docDir=%@ \n libDir=%@ \n cachesDir=%@ \n tmpDir=%@ \n bundle=%@", homeDir,docDir, libDir, cachesDir, tmpDir, bundle);
開始進入正題
1. plist
plist文件是將某些特定的類,通過XML文件的方式保存在目錄中。
可以被序列化的類型只有如下幾種:
NSArray;
NSMutableArray;
NSDictionary;
NSMutableDictionary;
NSData;
NSMutableData;
NSString;
NSMutableString;
NSNumber;
NSDate;
使用
- (void)plist{
//獲取文件路徑
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *fileName = [path stringByAppendingPathComponent:@"mygame.plist"];
//存儲
NSArray *array = @[@"紅色警戒", @"王者榮耀", @"穿越火線",@"英雄聯盟",@"超級瑪麗"];
[array writeToFile:fileName atomically:YES];
//讀取
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);
}
- 存儲時使用writeToFile: atomically:方法。 其中atomically表示是否需要先寫入一個輔助文件,再把輔助文件拷貝到目標文件地址。這是更安全的寫入文件方法,一般都寫YES。
2 偏好設置
-幾乎每個App都使用有偏好設置,如用戶名,密碼,字體大小等,iOS提供了一套標準的解決方案來為應用提供這項功能
- 每個應用都有NSUserDefaults單例,通過它來存取偏好設置
偏好設置專門用來保存應用程序的配置信息,通常不要在里面保存其它數據 - 如果利用系統的偏好設置存儲數據,默認保存在Preferences里,并且會將所有數據保存在同一個文件中
使用
- (void)userDefaults{
//1.獲得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向文件中寫入內容
[userDefaults setObject:@"小明" forKey:@"name"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];
//3.讀取文件
NSString *name = [userDefaults objectForKey:@"name"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
NSLog(@"%@, %d, %ld", name, sex, age);
}
- 如果沒有調用synchronize方法,系統會根據I/O情況不定時刻地保存到文件中。所以如果需要立即寫入文件的就必須調用synchronize方法。
- 偏好設置會將所有數據保存到同一個文件中。即preference目錄下的一個以此應用包名來命名的plist文件。
3. 歸解檔(NSKeyedArchiver)
歸檔可以實現把自定義的對象存入文件中,解決了plist和偏好設置只能存儲常用類型的致命缺陷,可以一次性取出對象的全部屬性
只要遵循了NSCoding協議的對象都可以通過它實現序列化,然后進行歸檔。
創建一個類,給它一些屬性,如果想將這個類保存到文件中,必須讓它遵守<NSCoding>
@interface Dog : NSObject<NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *color;
@end
在該類內部實現歸檔解檔方法,(將一個自定義對象保存到文件中或從文件中讀取的時候就會調用下面兩個方法)
@implementation Dog
//歸檔,告訴系統該對象的哪些屬性需要存儲
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
[aCoder encodeObject:self.color forKey:@"color"];
}
//解檔,告訴系統該對象的哪些屬性可以獲取
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
self.color = [aDecoder decodeObjectForKey:@"color"];
}
return self;
}
@end
歸檔和解檔
NSString * filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"DogList"];
[NSKeyedArchiver archiveRootObject:dog toFile:filePath];
Dog *dog = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
4.SQLite3
對于大量數據的存儲,就要用到數據庫,原生的SQLite3極其晦澀,不易懂,不易用。所有就有了第三方庫FMDB
我們就簡單介紹一下FMDB的用法。
在 FMDB 中有三個重要的類:
- FMDatabase:是一個提供 SQLite 數據庫的類,用于執行 SQL 語句。
- FMResultSet:用在FMDatabase中執行查詢的結果的類。
- FMDatabaseQueue:在多線程下查詢和更新數據庫用到的類。
建立一個Person
類,進行存儲
@property(nonatomic,copy) NSString *name;
@property(nonatomic,assign) NSInteger age;
@property(nonatomic,assign) NSInteger number;
#import "DataManager.h"
#import "FMDB.h"
@interface DataManager (){
FMDatabase *_db;
FMDatabaseQueue *_queue;
}
@end
@implementation DataManager
static id _instance;
+ (instancetype)shared{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc]init];
});
return _instance;
}
// 初始化數據
- (void)initDataBase{
// 獲得Documents目錄路徑
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
// 文件路
NSString *filePath = [documentsPath stringByAppendingPathComponent:@"model5.sqlite"];
_queue = [FMDatabaseQueue databaseQueueWithPath:filePath];
// 實例化FMDataBase對象
_db = [FMDatabase databaseWithPath:filePath];
if ([_db open]) {//'person_id' VARCHAR(255)
// 初始化數據表
NSString *personSql = @"CREATE TABLE 'person' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ,'id' VARCHAR(255),'name' VARCHAR(255),'age' VARCHAR(255),'number'VARCHAR(255)) ";
BOOL res = [_db executeUpdate:personSql];
if (res) {
NSLog(@"success to creating db table");
} else {
NSLog(@"error when creating db table");
}
[_db close];
} else {
NSLog(@"error when open db");
}
}
// 添加一條數據
//和insertStudent思路不一樣
- (void)addPerson:(Person *)person{
[_db open];
NSNumber *maxID = @(0);
FMResultSet *res = [_db executeQuery:@"SELECT * FROM person "];
//獲取數據庫中最大的ID
while ([res next]) {
if ([maxID integerValue] < [[res stringForColumn:@"id"] integerValue]) {
maxID = @([[res stringForColumn:@"id"] integerValue] ) ;
}
}
maxID = @([maxID integerValue] + 1);
[_db executeUpdate:@"INSERT INTO person(id,name,age,number) VALUES(?,?,?,?)",maxID,person.name,@(person.age),@(person.number)];
[_db close];
}
/// 添加一條數據
/// @param person person
- (void)insertStudent:(Person *)person{
NSLog(@"%s", __func__);
static int idx = 1;
// FMDatabase *db = [FMDatabase databaseWithPath:self.dbPath];
if ([_db open]) {
NSString *sql = @"insert into person (name, age, number) values(?, ?, ?) ";
BOOL res = [_db executeUpdate:sql, person.name, @(person.age), @(person.number)];
if (!res) {
NSLog(@"error to insert data");
} else {
NSLog(@"success to insert data");
}
[_db close];
}
}
/// 添加多條數據
/// @param persons persons
- (void)insertStudents:(NSArray <Person *> *)persons{
for (Person *p in persons) {
[self insertStudent:p];
}
}
/// 更新一條數據
/// @param person person
- (void)updateStudent:(Person *)person{
[_queue inDatabase:^(FMDatabase * _Nonnull db) {
//更新某個學生的年齡數據
BOOL result = [db executeUpdate:@"update t_person set age = ? where name = ? ;",@(person.age), person.name];
if (result) {
NSLog(@"更新學生數據 name = %@ age = %ld 成功",person.name, (long)person.age);
} else {
NSLog(@"更新學生數據 name = %@ age = %ld 失敗!",person.name, (long)person.age);
}
}];
}
/// 刪除一條數據
/// @param person person
- (void)deleteStudent:(Person *)person{
[_queue inDatabase:^(FMDatabase * _Nonnull db) {
BOOL result = [db executeUpdate:@"delete from t_person where name = ? and age = ? ;",person.name, @(person.age)];
if (result) {
NSLog(@"刪除學生數據 name = %@ age = %ld 成功",person.name, (long)person.age);
} else {
NSLog(@"刪除學生數據 name = %@ age = %ld 失敗!",person.name, (long)person.age);
}
}];
}
/// 查詢數據
- (NSArray *)queryStudents{
__block NSMutableArray *persons = [NSMutableArray array];
//inDatabase 內部是一個同步線程,所以在 block 執行完畢之前,查詢方法不會被提前 return
[_queue inDatabase:^(FMDatabase * _Nonnull db) {
//查詢年齡大于 20歲的學生數據, ASC為升序(默認), DESC 為降序
FMResultSet *rs = [db executeQuery:@"select * from t_student where age > ? order by age ASC;",@(20)];
//用 while
while (rs.next) {
NSString *name = [rs stringForColumn:@"name"];
int age = [rs intForColumn:@"age"];
int number = [rs intForColumn:@"number"];
Person *person = [[Person alloc] init];
person.name = name;
person.age = age;
person.number = number;
[persons addObject:person];
}
}];
[_db close];
return persons.copy;
}
- (NSArray *)getAllPerson{
NSLog(@"%s", __func__);
__block NSMutableArray *persons = [NSMutableArray array];
if ([_db open]) {
NSString *sql = @"select *from person";
FMResultSet *rs = [_db executeQuery:sql];
while ([rs next]) {
NSString *name = [rs stringForColumn:@"name"];
int age = [rs intForColumn:@"age"];
int number = [rs intForColumn:@"number"];
Person *person = [[Person alloc] init];
person.name = name;
person.age = age;
person.number = number;
[persons addObject:person];
}
}
[_db close];
return persons.copy;
}
@end
5. CoreData
CoreData是蘋果官方出品的一款用于存儲的數據庫,其底層也是封裝的SQlite3,但是比SQLite3更加面向對象。
但是,說實話,使用起來并不是非常的友好。