數據持久化方案
iOS 默認情況下只能訪問程序自己所在的目錄,稱為“沙盒”,沙盒結構的目錄如下:
-
Application 應用程序包,存放資源文件和可執行文件,上架前經過數字簽名,上架后不可修改
NSString *path = [[NSBundle mainBundle] bundlePath];
-
Documents: iCloud 備份目錄,存放數據,但不能存放緩存文件
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
Library/Caches: iCloud 不會備份此目錄,適合大體積、不需要備份的文件
Library/Preferences: 保存應用設置信息,iCloud 會備份
tmp: 臨時文件,隨時可能被刪除,iCloud 不會備份
數據持久化方案
- plist文件(屬性列表)
- preference(偏好設置)
- NSKeyedArchiver(歸檔)
- SQLite 3
- CoreData
1. plist 文件
plist 文件本質上是 xml 格式存儲的結構化數據,支持以下數據結構
- 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);
此方法返回的是一個字符串數組,NSSearchPathDirectory 就是沙盒結構內的目錄,NSSearchPathDomainMask 指定搜索范圍,這里的NSUserDomainMask表示搜索的范圍限制于當前應用的沙盒目錄。還可以寫成NSLocalDomainMask(表示/Library)、NSNetworkDomainMask(表示/Network)等。第三個參數表示是否將文件路徑補全,例如下面的代碼
NSString *path = [@"~/Documents/test" stringByExpandingTildeInPath];
NSLog(@"%@", path);
打印結果是
/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 的方法,還有一個方法可以用于選擇在目錄不存在的情況下創建該目錄
- (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 數組,拿到其中的 NSURL 后可以通過 url.path 獲取到路徑,然后補上文件名稱
NSString *fileName = [url.path stringByAppendingPathComponent:@"123.plist"];
存儲數據
NSArray *array = @[@"yasic", @"esir"];
[array writeToFile:fileName atomically:YES];
讀出數據
NSArray *readArray = [NSArray arrayWithContentsOfFile:fileName];
_resultLabel.text = [NSString stringWithFormat:@"%@ %@", readArray[0], readArray[1]];
2. preference
preference 本質上也是 xml 文件,存儲的應用設置相關的數據。
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 方法是用來同步內存中的數據到文件里的,如果不手動調用則系統會在合適的時候進行寫入操作。
3. NSKeyedArchiver
如果一個類遵循了 NSCoding 協議就可以進行歸檔和解檔操作。
- NSString
- NSNumber
- NSArray
- NSDictionary
- NSSet
- NSData
- UIColor
- UIImage
首先定義一個遵循協議的類并實現協議的歸檔和解檔方法
@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
要注意的是如果歸檔類是某個自定義類的子類時,就需要在歸檔和解檔之前先實現父類的歸檔和解檔方法。即 [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];
主要是調用 (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path; 方法進行存儲,調用 (nullable id)unarchiveObjectWithFile:(NSString *)path; 方法進行讀取。
4. CoreData
CoreData 是基于 sqlite 的封裝,最終將數據保存到一個數據庫文件中,同時提供了 ORM 功能。數據最終的存儲類型可以是:SQLite數據庫,XML,二進制,內存里,或自定義數據類型。
其工作原理是
- NSManagedObjectContext 臨時數據庫向 NSPersistentStoreCoordinator 持久化存儲助理發送一個key(model 名字)
- NSPersistentStoreCoordinator 通過這個 key 在 NSManagedObjectModel 數據模型中找到這個 model 對應的表
- NSManagedObjectModel 將這個表名返回給 NSPersistentStoreCoordinator
- NSPersistentStoreCoordinator 通過表名找到給表的 file 路徑
- NSPersistentStoreCoordinator 將這個路徑返回給 NSManagedObjectContext
- NSManagedObjectContext 對數據進行處理(增, 刪 , 該, 查)
首先新建自己的 Data Model 文件,會生成一個 .xcdatamodeld 文件,在文件中加入 Entity,然后對 Entity 加入 attributes,設置名稱和類型。然后要注意,在 Xcode8 中,默認的 NSManagedObject Subclass 是由類定義的,要自己定義需要把 Data Model Inspector 中的 Class Codegen 改成 none,然后選中 Data Model 文件,在 Editor 中選擇 “Create NSManagedObject Subclass”,這樣就將 Entity 變成了 oc 中的類,可以直接將類對應的對象與數據庫中的表對應起來。
然后是對 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 會將應用程序包中的 model 合并起來,coordinator 依據 model 生成,然后加入數據庫文件作為持久化庫,還可以指定存儲的類型。最后 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];
}
當然也可以對查詢的數據先進行排序再返回
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
request.sortDescriptors = @[sort];
最后注意,如果程序已經編譯運行過,再修改 Data Model 文件會報錯
NSPersistentStoreCoordinator has no persistent stores.
解決辦法是重新安裝一次應用,或者找到.sqlite將其刪掉。
5. FMDB
FMDB 是 SQLite 數據庫框架,是對 SQLite 的 C 語言 API 的封裝,對多線程操作進行了處理,是線程安全的。使用之前需要先導入 sqlite3.dylib 文件。
在 Linked Frameworks and Libraries 中加入 libsqlite3.tbd。
導入 sqlite3 的頭文件
#import "sqlite3.h"
FMDB 有三個重要的類
- FMDatabase:一個 FMDatabase 對象就代表一個單獨的 SQLite 數據庫(注意并不是表),用來執行 SQL 語句
- FMResultSet:使用 FMDatabase 執行查詢后的結果集
- FMDatabaseQueue:用于在多線程中執行多個查詢或更新,它是線程安全的
FMDB 需要使用 pod 添加依賴。
關于數據庫文件的文件路徑
- 具體文件路徑,如果不存在會自動創建
- 空字符串@"",會在臨時目錄創建一個空的數據庫,當FMDatabase連接關閉時,數據庫文件也被刪除
- nil,會創建一個內存中臨時數據庫,當FMDatabase連接關閉時,數據庫會被銷毀
初始化和創建表
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 方法中傳遞的參數必須是 objectivec 對象,最典型的 NSInteger 是非 oc 對象,會發生運行時錯誤。