数据持久化方案
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 对象,会发生运行时错误。