公司的项目存在已有两年,版本也到三点几了,但是本地持久化iOS数据存储,始终用的是GVUserDefaults这个对NSUserDefaults进行了扩展的第三方库。但随着业务的发展,需要存储的地方越来越多,GVUserDefaults也越来也不能适应需求,当我们都忍受不了的时候,经过一番商讨之后,决定使用FMDB这个封装了SQLite3的第三方库。此篇文章以此为主线,理一理数据库和本地化储存的一些知识,但是此篇文章丝毫没有提到CoreData,喜欢使用CoreData的同学,这里先说声对不起了浪费了您20s宝贵的时间。
此篇文章的逻辑如下图所示:
说起iOS本地化储存的方式,大家估计在也熟悉不过了,NSUserDefault、File,Keychain、DataBase无非也就这几种方式。
首先要有数据Data,然后数据多了名字就升级了称为数据库DataBase,这个时候就需要有管理系统去管理数据库也就是DataBaseManagerSystem,最后佐以DataBaseAdministrator及上述名称成为了一个系统就是所谓的DataBaseSystem。
Data --> DataBase --> DataBaseManagerSystem --> DataBaseSystem
SQL(Structured Query Language),结构化查询语言,专门用来与数据库通信的语言。既然要操作数据库,SQL语句是必须要会写的。下面我简单列举几条简单的SQL语句仅供参考。想要学习更多的SQL语句的知识,请查阅其他资料,本篇在这儿不过多叙述。
// 创表(table)一张学生表 表名:student
字段id: 学生编号,作为主键,类型为整形
字段name:学生名字,类型为字符串,并且不能为空值
字段age: 学生年龄,类型为整形可为空。
其中字段又称之为列(column)create table if not exists student (
id integer primary key autoincrement,
name text not null,
age integer);
// 删除学生表drop table if exists student;
// 插入一条记录,主键id会自增长自动赋值,
其中记录又称之为行(row)insert into student (name, age) values ('小明', 20);
// 删除名字为小明的学生,
这里的关键字where就是条件,如果无此条件,则影响整张表delete from student where name = '小明';
// 更新符合条件的记录update student set age = 21 where name = '小明';
// 查询符合条件的记录select * from student where name = '小明';
如果你没有使用CoreData,那么无论你使用的是纯C语言的SQLite3库,还是使用对其进行封装了的FMDB,你都要设计出适合自己业务的表结构。关于表的设计,可以有两种设计模式。
以模型中的每个属性作为表的一个字段,这样在查询、读取放面操作起来更为方便,但是我个人感觉这种模式适合的业务是记录条数不多。而且字段尽可能的不要更改。创建好表之后,在去修改表结构还是听麻烦的一件事的。具体使用,可参考下面的代码:
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) NSUInteger age;
@end
// 数据储存的路径NSString *document = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) lastObject];NSString *dbpath = [document stringByAppendingPathComponent:@"toyun.sqlite"];// 链接数据库self.db = [FMDatabase databaseWithPath:dbpath];
// 打开数据库if ([self.db open]) {
[self.db executeUpdate:@"create table if not exists student ( id integer primary key autoincrement, name text not null, age integer not null)"];
}
// 实例化对象,即要储存的对象NSMutableArray *array = [NSMutableArray array];for (NSInteger i = 0; i < 10; i++) {
Student *student = [[Student alloc] init];
student.name = [NSString stringWithFormat:@"小明%ld", i];
student.age = arc4random() % 20 + 10;
[array addObject:student];
}
// 将数据库插入表中for (Student *student in array) {
[self.db executeUpdateWithFormat:@"insert into student (name, age) values (%@, %ld)", student.name, student.age];
}
[self.db close];NSLog(@"dbpath === %@", dbpath);
这一种设计模式是将model作为一个字段直接将model转为NSData储在此字段中,在这里要指出的是model必须实现NSCoding协议,不过实际项目中我们字典转模型大都是使用第三方库,而现在比较流行的三个字典转模型的第三方库Mantle、MJExtension、YYModel都默认对此进行了处理,所以这儿稍微注意一下就好。第二种方法更适合于记录条数比较多,和业务的相关性不是太敏感,而且如果有查找排序这样的需求的话,可以从model挑出某些属性作为表中附带的一些字段。具体使用,参考下面的代码:
@interface Student : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) NSUInteger age;
@end
@implementation Student
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_name forKey:@"name"];
[aCoder encodeInteger:_age forKey:@"age"];
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_name = [aDecoder decodeObjectForKey:@"name"];
_age = [aDecoder decodeIntegerForKey:@"age"];
}
return self;
}@end
NSString *document = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];NSString *dbpath = [document stringByAppendingPathComponent:@"yeemiao.sqlite"];self.db = [FMDatabase databaseWithPath:dbpath];
if ([self.db open]) {
[self.db executeUpdate:@"create table if not exists student (id integer primary key autoincrement, model blob)"];
}
NSMutableArray *array = [NSMutableArray array]; for (NSInteger i = 0; i < 10; i++) {
Student *student = [[Student alloc] init];
student.name = [NSString stringWithFormat:@"小明%ld", i];
student.age = arc4random() % 20 + 10;
[array addObject:student];
}
// 转为NSData,存入数据库for (Student *student in array) {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:student];
[self.db executeUpdateWithFormat:@"insert into student (model) values (%@)", data];
}
// 从数据库中查数据
FMResultSet *set = [self.db executeQuery:@"select * from student"];NSMutableArray *resultArray = [NSMutableArray array];while (set.next) {
NSData *data = [set objectForColumnName:@"model"];
Student *student = [NSKeyedUnarchiver unarchiveObjectWithData:data];
[resultArray addObject:student];
}
[self.db close];
在多线程中操作数据库,我们就要考虑线程安全问题,不然就有可能引起数据错乱的问题。解决办法有很多,可以自己去加锁,但是读写速度要求高的话就不太建议加锁了。SQLite3对于多线程是直接支持的,SQLite3库提供了三种方式:single thread、multi thread、serialized。同样 FMDB自己也提供了多线程操作数据库的类FMDatabaseQueue,这个使用起来还是比较简单的。
现在大多数的App都是有本地化储存的,但是却不是每个App都对应有删除缓存的功能,起码我们自己的App是没有做删除缓存这个功能的,缓存是为了节省流量,删除缓存是为了节省空间我认为这两个功能同等重要,但是这个需求提了几次,产品不怎么重视啊!特别是在16G的版本下,这个功能还能能提升一些用户体验的。
此功能到也不难实现,搞清楚各种储存方式他们把文件存储到了那个文件夹下面,不同的数据我们储存的地方明显也是不同的,然后根据需求删除对应的数据就OK了,沙盒的具体目录如下所示:
// 沙盒下的文件夹目录
Documents: iTunes会同步此文件夹,不应该删除
Library
Caches: 缓存文件夹,删除缓存主要删除的文件夹
Preferences: NSUserDefault写入此文件夹下,iTunes会同步,不应该删除
tmp:系统创建的临时文件夹,随时有可能被删除
做清除缓存功能时,可把Library/Caches文件夹下面的文件全部删除,也可以根据业务的需要删除指定的文件夹。
此篇文章还是比较偏重于原理,对于具体的使用则没有说太多,具体使用各种各样,但是基本原理是比较统一的。关于数据库CURD由于我们不是做服务端开发,所以也没有深究,想要研究的话,可在查阅其他资料。除了上述所说的,可能在具体使用中还要考虑数据库版本迁移,数据库同步等需求。这些由于我的水平所限,也没有提到可以参考我写此文时的一篇参考博文iOS应用架构谈 本地持久化方案及动态部署。而NSUserDefault、File使用方法都很简单也都没有介绍。CoreData太过于重量级,我在项目中也没事实际运用过,这里也没有介绍。
原文来自: 简书/TIME_for