经过一步一个脚印的开发,咱们实现了数据库的增删查改,并支持多种类型数据存储,如:全部基本数据类型,NSArray,NSMutableArray,NSDictionary,NSMutableDictionary,UIImage,NSURL,UIColor,NSSet,NSRange,NSAttributedString,NSData,自定义模型以及数组、字典、模型相互嵌套的复杂场景。html
而后咱们很是完美的将打开数据库,建立数据库表格,解析模型对象,插入数据,更新数据,数据库升级,数据迁移,关闭数据库一系列步骤封装成一个方法,一行代码智能实现复杂模型的数据存储。若是想了解各个部分是如何实现的,能够前往以前的文章,传送门:git
本篇主要解决多线程安全问题,而后会随着讲一下经常使用的多线程安全技术以及关于在ARC下使用@autorelease的一个必要的场景,最后会分享咱们在进行单元测试的时候遇到的一个小坑。github
实现功能以前,咱们先知道多线程安全要作什么?简单的来讲,咱们就是要保证在多个线程同时对数据库进行操做的时候是安全的,也能够说咱们要保证全部数据库的操做无论从哪一个线程过来都要等前面的操做执行完毕再执行本操做,避免资源竞争和冲突。sql
而后咱们要去了解一下OC下保证多线程安全的手段,对于OC咱们最多见的有原子性atomic,而后有NSLock锁、@synchronized、GCD的信号量、串行队列。数据库
当咱们在纠结选择何种方案的时候,咱们能够先去看看前辈们的开源是如何作数据库线程安全的,借鉴一下,最终咱们总结出两个比较优秀的方案:一种方案是FMDB所使用的同步串行队列:全部的操做都用一个串行的队列排好,一个个操做排队进行。另外一种是使用GCD信号量dispatch_semaphore_t。结合咱们以前写的代码,根据咱们目前的数据库方案,快速对比一下哪一种更适合咱们,最终咱们选择了GCD信号量,使用这个方案,咱们的代码基本不用变更。数组
在GCD中有三个函数是semaphore的操做,分别是:缓存
简单的介绍一下这三个函数:安全
dispatch_samaphore_t dispatch_semaphore_create(long value);
这个函数有一个长整形的参数,咱们能够理解为信号的总量;
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
这个函数第一个参数为信号量,第二个为等待的时间,这个函数的做用是这样的:
若是dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,而且将信号量的值减1;
若是desema的值小于等于0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t);
若是在等待的期间desema大于0了,则向下执行操做并讲信号量减1.
long dispatch_semaphore_signal(dispatch_semaphore_tdsema)
这个函数会使传入的信号量dsema的值加1;
复制代码
同时考虑到咱们目前的方法都是类方法,咱们须要一个实例来记住desema的值,因而咱们给CWSqliteModelTool开启一个单例对象来记录desema的值,设置信号总量为1,以后在每个执行数据库操做的的方法开始前进行等待信号量,若是当前信号量大于0,咱们执行操做数据库,并讲信号量减1,当操做完成以后,咱们发送信号,使信号量增1,这样使其余在等待的线程能开始执行操做,以执行查询操做为例:bash
- (instancetype)init
{
self = [super init];
if (self) {
// 设置信号量为1,表示最多同时只有1个线程进行操做
self.dsema = dispatch_semaphore_create(1);
}
return self;
}
// 建立一个单例,来记录信号量的值
static CWSqliteModelTool * instance = nil;
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[CWSqliteModelTool alloc] init];
});
return instance;
}
// 查询表内全部数据
+ (NSArray *)queryAllModels:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId {
// 等待信号量,若是大于0,向下执行操做,不然等待
dispatch_semaphore_wait([[self shareInstance] dsema], DISPATCH_TIME_FOREVER);
NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
NSString *sql = [NSString stringWithFormat:@"select * from %@", tableName];
NSArray <NSDictionary *>*results = [CWDatabase querySql:sql uid:uid];
[CWDatabase closeDB];
// 发送信号量,使信号量+1
dispatch_semaphore_signal([[self shareInstance] dsema]);
return [self parseResults:results withClass:cls];
}
复制代码
咱们在其余的方法内写上一样的代码,而后咱们使用插入数据与删除数据的方法进行单元测试,测试方案为,开启3条子线程,每条线程分别插入1000个复杂的模型,当数据插入结束的时候,再使用3条子线程删除其中的2900条数据,最后剩下100条数据,而后咱们来进行单元测试:多线程
在使用单元测试时,分享一个咱们发现的坑~咱们发现一条数据都没插入成功或者偶尔插入了一两条数据,咱们反复检测咱们的代码,理论上都是没问题的,最终咱们定位到子线程队列的任务压根没执行,思考以后最终咱们得出结论:单元测试是在主线程运行,咱们使用异步线程时并不会阻塞主线程的运行,因此这个测试用例顺畅无阻的从第一行执行到了最后一行,而单元测试执行完最后一行以后程序就退出了,程序都退出了咱们异步的线程的操做固然无法再执行了~
因此咱们不能使用单元测试(或者在单元测试本身开启一个runloop让程序不退出),改在程序内进行测试,贴上咱们很是长的测试代码(viewController内):
#pragma mark - for循环未使用autoreleasepool的多线程操做
- (void)testMultiThreadingSqliteMore {
dispatch_queue_t queue1 = dispatch_queue_create("CWDBTest1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("CWDBTest2", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue3 = dispatch_queue_create("CWDBTest3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue4 = dispatch_queue_create("CWDBTest4", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_group_enter(group);
dispatch_group_enter(group);
dispatch_async(queue1, ^{
for (int i = 1; i < 1000; i++) {
Student *stu = [self studentWithId:i];
BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:nil];
NSLog(@"result : %d %zd",result,stu.stuId);
}
NSLog(@"---------------组1结束");
dispatch_group_leave(group);
});
dispatch_async(queue2, ^{
for (int i = 1000; i < 2000; i++) {
Student *stu = [self studentWithId:i];
BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:nil];
NSLog(@"result : %d %zd",result,stu.stuId);
}
NSLog(@"---------------组2结束");
dispatch_group_leave(group);
});
dispatch_async(queue3, ^{
for (int i = 2000; i < 3000; i++) {
Student *stu = [self studentWithId:i];
BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:nil];
NSLog(@"result : %d %zd",result,stu.stuId);
}
NSLog(@"---------------组3结束");
dispatch_group_leave(group);
});
// 当前面3个队列的任务都完成,则调用此通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"----------------------插入结束");
dispatch_async(queue4, ^{
for (int i = 1; i < 1000; i++) {
Student *stu = [self studentWithId:i];
// 删除数据
BOOL result = [CWSqliteModelTool deleteModel:stu uid:@"Chavez" targetId:nil];
NSLog(@"delete result : %d %zd",result,stu.stuId);
}
});
dispatch_async(queue1, ^{
for (int i = 2000; i < 3000; i++) {
Student *stu = [self studentWithId:i];
// 删除数据
BOOL result = [CWSqliteModelTool deleteModel:stu uid:@"Chavez" targetId:nil];
NSLog(@"delete result : %d %zd",result,stu.stuId);
}
});
dispatch_async(queue2, ^{
// 删除数据
BOOL result = [CWSqliteModelTool deleteModel:[Student class] columnNames:@[@"stuId",@"stuId"] relations:@[@(CWDBRelationTypeMoreEqual),@(CWDBRelationTypeLess)] values:@[@(1000),@(1900)] isAnd:YES uid:@"Chavez" targetId:nil];
NSLog(@"delete result : %d 1000-1900",result);
});
});
}
#pragma mark - 快速获取一个模型
- (Student *)studentWithId:(int)stuId {
School *school1 = [[School alloc] init];
school1.name = @"北京大学";
school1.schoolId = 2;
School *school = [[School alloc] init];
school.name = @"清华大学";
school.schoolId = 1;
school.school1 = school1;
Student *stu = [[Student alloc] init];
stu.stuId = stuId;
stu.name = @"Baidu";
stu.age = 100;
stu.height = 190;
stu.weight = 140;
stu.dict = @{@"name" : @"chavez"};
// 字典嵌套模型
stu.dictM = [@{@"清华大学" : school , @"北京大学" : school1 , @"money" : @(100)} mutableCopy];
// 数组嵌套字典,字典嵌套模型
stu.arrayM = [@[@"chavez",@"cw",@"ccww",@{@"清华大学" : school}] mutableCopy];
// 数组嵌套模型
stu.array = @[@(1),@(2),@(3),school,school1];
NSAttributedString *attributedStr = [[NSAttributedString alloc] initWithString:@"attributedStr,attributedStr"];
stu.attributedString = attributedStr;
// 模型嵌套模型
stu.school = school;
UIImage *image = [UIImage imageNamed:@"001"];
NSData *data = UIImageJPEGRepresentation(image, 1);
stu.image = image;
stu.data = data;
return stu;
}
复制代码
而后执行,获取测试结果,可是在反复侧测试的时候,咱们发现一个问题,以下图:
在咱们解决了多线程安全的问题以后咱们发现,既然可能有存在须要批量插入数据的状况,咱们就多增长一个接口来处理批量插入操做,批量插入实际上就是咱们替用户进行for循环插入,可是在插入的过程当中,咱们用事务控制插入操做,而且插入过程比用户少的是每次插入数据都要执行打开和关闭数据库操做以及每次都去检测是否须要更新数据库表。
#pragma mark - 测试批量插入数据
- (void)testGroupInsert {
NSMutableArray *arr = [NSMutableArray array];
for (int i = 1; i < 2000; i++) {
Student *stu = [self studentWithId:i];
[arr addObject:stu];
}
NSLog(@"开始插入数据");
// 2017-12-23 16:25:46.145023+0800 CWDB[14678:1604328] 开始插入数据
BOOL result = [CWSqliteModelTool insertOrUpdateModels:arr uid:@"Chavez" targetId:nil];
NSLog(@"---%zd---插入结束",result);
// 2017-12-23 16:25:48.466352+0800 CWDB[14678:1604328] ---1---插入结束
// 使用批量插入的方法 插入2000条数据,总共耗时2.3秒
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"---------------组1开始");
// 2017-12-23 16:25:48.466587+0800 CWDB[14678:1604407] ---------------组1开始
for (int i = 2000; i < 4000; i++) {
@autoreleasepool {
Student *stu = [self studentWithId:i];
BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:nil];
NSLog(@"result : %d %zd",result,stu.stuId);
}
}
NSLog(@"---------------组1结束");
// 2017-12-23 16:25:56.247631+0800 CWDB[14678:1604407] ---------------组1结束
// 自行遍历的方式插入2000条数据,总共耗时8秒(且要自行增长autoreleasepool释放临时变量)
});
}
复制代码
最终咱们经过批量插入以及单个插入2000条数据的时间比较,批量插入消耗的时间远低于单个分别插入。
在写完功能以后,咱们对项目又进行了一小部分缓存优化,在不考虑动态给模型添加属性的状况下,咱们每次去获取模型成员变量以及类型必定是同样的,因此,首先咱们在这里使用NSCache来缓存了模型的全部成员变量名以及类型,这样能够不用每次都去解析模型。其次,在判断数据库表结构是否须要更新的时候,咱们也作了缓存,在程序运行期间,只要更新过一次,后面都不用去判断更新,由于成员变量不变,表结构必定不会变,这都是对性能方面的一些小优化,在作的时候能够适当的考虑一下。
在此,咱们封装的数据库功能已经开发完了(其实本身封装一个数据库也没想象中那么难,你也能够的~)回到第一篇文章所立的军令状:咱们封装的简单适用,安全可靠,功能全面,咱们说到作到。增删查改全部操做都只须要一行代码。就算你给数组添加一个对象也须要两步:先初始化一个数组,而后再想数组添加对象,而咱们向数据库插入一个模型,只须要一步,调用insertOrUpdateModel:便可。
在接下来,咱们会将封装的代码进行少许的重构和优化,去掉一些没必要要暴露的方法和对应的单元测试,尽可能让API简洁明了以及去掉一些重复代码的封装,将注释补全再通过大量的测试场景测试以后,咱们将会把咱们的CWDB推荐给你们使用,若是你有兴趣了解或者想本身动手封装一个数据库,能够前往本系列文章第一篇开始看一看(文章的链接在本文的开头),每篇文章对应的代码在github的release下都有分别的tag,你能够找到他而且下载下来。。
本篇文章实现的代码地址:github:CWDB ------tag为1.4.0-------
(注意:若是要直接运行,必须在CWDatabase.m开头的位置修改数据库存放的路径,开发调试阶段我写在了我电脑的桌面,不修改会出现路径错误,致使打开数据库失败)
最后以为有用的同窗,但愿能给本文点个喜欢,给github点个star以资鼓励,谢谢你们。欢迎你们向我抛issue,有更好的思路也欢迎你们留言。
给你们安利一个0耦合的仿QQ侧滑框架,真正的一行代码实现,多了你抽我😁: 一行代码集成超低耦合的侧滑功能