持久化,就是将数据保存起来。使得下次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更加面向对象。
但是,说实话,使用起来并不是非常的友好。