數(shù)據(jù)持久化方案
iOS 默認(rèn)情況下只能訪問程序自己所在的目錄淆珊,稱為“沙盒”椭盏,沙盒結(jié)構(gòu)的目錄如下:
-
Application 應(yīng)用程序包年栓,存放資源文件和可執(zhí)行文件纺腊,上架前經(jīng)過數(shù)字簽名畔师,上架后不可修改
NSString *path = [[NSBundle mainBundle] bundlePath];
-
Documents: iCloud 備份目錄娶靡,存放數(shù)據(jù),但不能存放緩存文件
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
Library/Caches: iCloud 不會備份此目錄看锉,適合大體積姿锭、不需要備份的文件
Library/Preferences: 保存應(yīng)用設(shè)置信息,iCloud 會備份
tmp: 臨時文件伯铣,隨時可能被刪除呻此,iCloud 不會備份
數(shù)據(jù)持久化方案
- plist文件(屬性列表)
- preference(偏好設(shè)置)
- NSKeyedArchiver(歸檔)
- SQLite 3
- CoreData
1. plist 文件
plist 文件本質(zhì)上是 xml 格式存儲的結(jié)構(gòu)化數(shù)據(jù),支持以下數(shù)據(jù)結(jié)構(gòu)
- NSArray
- NSMutableArray
- NSDictionary
- NSMutableDictionary
- NSData
- NSMutableData
- NSString
- NSMutableString
- NSNumber
- NSDate
獲取路徑
首先獲取到存儲路徑和文件名稱
//NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *url = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
NSString *fileName = [url.path stringByAppendingPathComponent:@"123.plist"];
這里獲取文件路徑有兩種方式
NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
此方法返回的是一個字符串?dāng)?shù)組腔寡,NSSearchPathDirectory 就是沙盒結(jié)構(gòu)內(nèi)的目錄趾诗,NSSearchPathDomainMask 指定搜索范圍,這里的NSUserDomainMask表示搜索的范圍限制于當(dāng)前應(yīng)用的沙盒目錄蹬蚁。還可以寫成NSLocalDomainMask(表示/Library)恃泪、NSNetworkDomainMask(表示/Network)等。第三個參數(shù)表示是否將文件路徑補全犀斋,例如下面的代碼
NSString *path = [@"~/Documents/test" stringByExpandingTildeInPath];
NSLog(@"%@", path);
打印結(jié)果是
/var/mobile/Containers/Data/Application/7CCD2BE1-9092-4A8C-81C7-348ABC960013/Documents/test
另一種獲取文件路徑的方法是
- (NSArray<NSURL *> *)URLsForDirectory:(NSSearchPathDirectory)directory inDomains:(NSSearchPathDomainMask)domainMask NS_AVAILABLE(10_6, 4_0);
它是 NSFileManager 的方法贝乎,還有一個方法可以用于選擇在目錄不存在的情況下創(chuàng)建該目錄
- (nullable NSURL *)URLForDirectory:(NSSearchPathDirectory)directory inDomain:(NSSearchPathDomainMask)domain appropriateForURL:(nullable NSURL *)url create:(BOOL)shouldCreate error:(NSError **)error NS_AVAILABLE(10_6, 4_0);
蘋果官方文檔推薦用 FileManager 的方法獲取文件目錄,獲取到的是一個 NSURl 數(shù)組叽粹,拿到其中的 NSURL 后可以通過 url.path 獲取到路徑览效,然后補上文件名稱
NSString *fileName = [url.path stringByAppendingPathComponent:@"123.plist"];
存儲數(shù)據(jù)
NSArray *array = @[@"yasic", @"esir"];
[array writeToFile:fileName atomically:YES];
讀出數(shù)據(jù)
NSArray *readArray = [NSArray arrayWithContentsOfFile:fileName];
_resultLabel.text = [NSString stringWithFormat:@"%@ %@", readArray[0], readArray[1]];
2. preference
preference 本質(zhì)上也是 xml 文件,存儲的應(yīng)用設(shè)置相關(guān)的數(shù)據(jù)虫几。
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:@"Male" forKey:@"Yasic"];
[userDefaults setObject:@"Famle" forKey:@"Esir"];
[userDefaults synchronize];
NSString *male = [userDefaults objectForKey:@"Yasic"];
NSString *famale = [userDefaults objectForKey:@"Esir"];
_resultLabel.text = [NSString stringWithFormat: @"yasic %@, esir %@", male, famale];
要注意 synchronize 方法是用來同步內(nèi)存中的數(shù)據(jù)到文件里的锤灿,如果不手動調(diào)用則系統(tǒng)會在合適的時候進(jìn)行寫入操作。
3. NSKeyedArchiver
如果一個類遵循了 NSCoding 協(xié)議就可以進(jìn)行歸檔和解檔操作辆脸。
- NSString
- NSNumber
- NSArray
- NSDictionary
- NSSet
- NSData
- UIColor
- UIImage
首先定義一個遵循協(xié)議的類并實現(xiàn)協(xié)議的歸檔和解檔方法
@interface Person : NSObject<NSCoding>
@property(strong, nonatomic) NSString *name;
@property(strong, nonatomic) NSString *gender;
@property(assign, nonatomic) NSInteger age;
@end
@implementation Person
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if ([super init])
{
self.name = [aDecoder decodeObjectForKey:@"name"];
self.gender = [aDecoder decodeObjectForKey:@"gender"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:self.gender forKey:@"gender"];
[aCoder encodeInteger:self.age forKey:@"age"];
}
@end
要注意的是如果歸檔類是某個自定義類的子類時但校,就需要在歸檔和解檔之前先實現(xiàn)父類的歸檔和解檔方法。即 [super encodeWithCoder:aCoder]
和 [super initWithCoder:aDecoder]
方法啡氢。
具體的存儲和讀寫操作如下
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *url = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
NSString *fileName = [url.path stringByAppendingPathComponent:@"person.data"];
Person *yasic = [[Person alloc] init];
yasic.name = @"Yasic";
yasic.gender = @"Male";
yasic.age = 22;
[NSKeyedArchiver archiveRootObject:yasic toFile:fileName];
Person *myYasic = [NSKeyedUnarchiver unarchiveObjectWithFile:fileName];
_resultLabel.text = [NSString stringWithFormat:@"%@ %@ %ld", myYasic.name, myYasic.gender, myYasic.age];
主要是調(diào)用 (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path; 方法進(jìn)行存儲状囱,調(diào)用 (nullable id)unarchiveObjectWithFile:(NSString *)path; 方法進(jìn)行讀取术裸。
4. CoreData
CoreData 是基于 sqlite 的封裝,最終將數(shù)據(jù)保存到一個數(shù)據(jù)庫文件中亭枷,同時提供了 ORM 功能袭艺。數(shù)據(jù)最終的存儲類型可以是:SQLite數(shù)據(jù)庫,XML叨粘,二進(jìn)制猾编,內(nèi)存里,或自定義數(shù)據(jù)類型升敲。
其工作原理是
- NSManagedObjectContext 臨時數(shù)據(jù)庫向 NSPersistentStoreCoordinator 持久化存儲助理發(fā)送一個key(model 名字)
- NSPersistentStoreCoordinator 通過這個 key 在 NSManagedObjectModel 數(shù)據(jù)模型中找到這個 model 對應(yīng)的表
- NSManagedObjectModel 將這個表名返回給 NSPersistentStoreCoordinator
- NSPersistentStoreCoordinator 通過表名找到給表的 file 路徑
- NSPersistentStoreCoordinator 將這個路徑返回給 NSManagedObjectContext
- NSManagedObjectContext 對數(shù)據(jù)進(jìn)行處理(增, 刪 , 該, 查)
首先新建自己的 Data Model 文件袍镀,會生成一個 .xcdatamodeld 文件,在文件中加入 Entity冻晤,然后對 Entity 加入 attributes,設(shè)置名稱和類型绸吸。然后要注意鼻弧,在 Xcode8 中,默認(rèn)的 NSManagedObject Subclass 是由類定義的锦茁,要自己定義需要把 Data Model Inspector 中的 Class Codegen 改成 none攘轩,然后選中 Data Model 文件,在 Editor 中選擇 “Create NSManagedObject Subclass”码俩,這樣就將 Entity 變成了 oc 中的類度帮,可以直接將類對應(yīng)的對象與數(shù)據(jù)庫中的表對應(yīng)起來。
然后是對 context稿存,coordinator 和 model 的初始化過程笨篷。
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];//如果是nil則表示mainbundles
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *fileName = [filePath stringByAppendingPathComponent:@"person.db"];
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:fileName] options:nil error:nil];
context.persistentStoreCoordinator = coordinator;
_manegeObjectContext = context;
context 直接初始化,model 會將應(yīng)用程序包中的 model 合并起來瓣履,coordinator 依據(jù) model 生成率翅,然后加入數(shù)據(jù)庫文件作為持久化庫,還可以指定存儲的類型袖迎。最后 context 持有 coordinator冕臭,coordinator 持有 model,就完成了初始化過程燕锥。
接下來是增刪查改的具體步驟辜贵。
增
- (IBAction)add:(UIButton *)sender {
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:_manegeObjectContext];
person.name = _nameField.text;
person.gender = _genderField.text;
person.age = [_ageField.text intValue];
NSError *error = nil;
[_manegeObjectContext save:&error];
if (error)
{
NSLog(@"%@", error);
}
}
這里 Person 就是通過 NSManagedObject Subclass 生成的類。
刪
- (IBAction)delete:(UIButton *)sender {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", _nameField.text];
request.predicate = predicate;
NSArray *results = [_manegeObjectContext executeFetchRequest:request error:nil];
Person *resultPerson = results[0];
[_manegeObjectContext deleteObject:resultPerson];
NSError *error = nil;
[_manegeObjectContext save:&error];
}
查
- (IBAction)query:(UIButton *)sender {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", _nameField.text];
request.predicate = predicate;
NSError *error = nil;
NSArray *results = [_manegeObjectContext executeFetchRequest:request error:&error];
if (error)
{
NSLog(@"%@", error);
}
if ([results count] > 0)
{
Person *resultPerson = results[0];
_resultLabel.text = [NSString stringWithFormat:@"%@ %@ %hd", resultPerson.name, resultPerson.gender, resultPerson.age];
}
else
{
_resultLabel.text = @"null";
}
}
查詢還支持模糊搜索和分頁搜索归形。
改
- (IBAction)Update:(UIButton *)sender {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", _nameField.text];
request.predicate = predicate;
NSArray *results = [_manegeObjectContext executeFetchRequest:request error:nil];
Person *resultPerson = results[0];
NSError *error = nil;
resultPerson.age = [_ageField.text intValue];
resultPerson.gender = _genderField.text;
[_manegeObjectContext save:&error];
}
當(dāng)然也可以對查詢的數(shù)據(jù)先進(jìn)行排序再返回
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
request.sortDescriptors = @[sort];
最后注意托慨,如果程序已經(jīng)編譯運行過,再修改 Data Model 文件會報錯
NSPersistentStoreCoordinator has no persistent stores.
解決辦法是重新安裝一次應(yīng)用暇榴,或者找到.sqlite將其刪掉榴芳。
5. FMDB
FMDB 是 SQLite 數(shù)據(jù)庫框架嗡靡,是對 SQLite 的 C 語言 API 的封裝,對多線程操作進(jìn)行了處理窟感,是線程安全的讨彼。使用之前需要先導(dǎo)入 sqlite3.dylib 文件。
在 Linked Frameworks and Libraries 中加入 libsqlite3.tbd柿祈。
導(dǎo)入 sqlite3 的頭文件
#import "sqlite3.h"
FMDB 有三個重要的類
- FMDatabase:一個 FMDatabase 對象就代表一個單獨的 SQLite 數(shù)據(jù)庫(注意并不是表)哈误,用來執(zhí)行 SQL 語句
- FMResultSet:使用 FMDatabase 執(zhí)行查詢后的結(jié)果集
- FMDatabaseQueue:用于在多線程中執(zhí)行多個查詢或更新,它是線程安全的
FMDB 需要使用 pod 添加依賴。
關(guān)于數(shù)據(jù)庫文件的文件路徑
- 具體文件路徑躏嚎,如果不存在會自動創(chuàng)建
- 空字符串@""蜜自,會在臨時目錄創(chuàng)建一個空的數(shù)據(jù)庫,當(dāng)FMDatabase連接關(guān)閉時卢佣,數(shù)據(jù)庫文件也被刪除
- nil重荠,會創(chuàng)建一個內(nèi)存中臨時數(shù)據(jù)庫,當(dāng)FMDatabase連接關(guān)閉時虚茶,數(shù)據(jù)庫會被銷毀
初始化和創(chuàng)建表
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
_db = [FMDatabase databaseWithPath:path];
if (![_db open])
{
NSLog(@"fail!");
}
BOOL result = [_db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_person (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL, gender text NOT NULL);"];
if (result)
{
NSLog(@"create successful.");
}
else
{
NSLog(@"create fail");
}
增
- (void)addItem
{
NSString *name = _name.text;
NSString *gender = _gender.text;
NSInteger age = [_age.text intValue];
BOOL result = [_db executeUpdate:@"INSERT INTO t_person (name, age, gender) VALUES (?,?,?)",name, @(age), gender];
if (result)
{
NSLog(@"successful %@", NSStringFromSelector(_cmd));
}
else
{
NSLog(@"fail %@", NSStringFromSelector(_cmd));
}
}
查
- (void)queryItem
{
FMResultSet *set = [_db executeQuery:@"select * from t_person where name = ?", _name.text];
NSString *result = @"";
NSString *temp = @"";
_resultLabel.text = @"";
while ([set next]) {
NSInteger id = [set intForColumn:@"id"];
NSString *name = [set objectForColumn:@"name"];
NSString *gender = [set objectForColumn:@"gender"];
NSInteger age = [set intForColumn:@"age"];
temp = [NSString stringWithFormat:@"%ld %@ %@ %ld", id, name, gender, age];
_resultLabel.text = temp;
result = [NSString stringWithFormat:@"%@\n%@", result, temp];
}
}
改
- (void)updateItem
{
BOOL result = [_db executeUpdate:@"update t_person set age = ?, gender = ? where name = ?", _age.text, _gender.text, _name.text];
if (result)
{
NSLog(@"successful %@", NSStringFromSelector(_cmd));
}
else
{
NSLog(@"error %@", NSStringFromSelector(_cmd));
}
}
刪
- (void)deleteItem
{
BOOL result = [_db executeUpdate:@"delete from t_person where name = ?", _name.text];
if (result)
{
NSLog(@"successful %@", NSStringFromSelector(_cmd));
}
else
{
NSLog(@"error %@", NSStringFromSelector(_cmd));
}
}
隊列操作
- (void)queueUpdateItem
{
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
[queue inDatabase:^(FMDatabase *database)
{
[database executeUpdate:@"update t_person set age = ? where name = ?", @(age), @"yasic1"];
[database executeUpdate:@"update t_person set age = ? where name = ?", @(age), @"yasic2"];
[database executeUpdate:@"update t_person set age = ? where name = ?", @(age), @"yasic3"];
}];
}
必須注意戈鲁,在所有 FMDB 方法中傳遞的參數(shù)必須是 objectivec 對象,最典型的 NSInteger 是非 oc 對象嘹叫,會發(fā)生運行時錯誤婆殿。