前言
最近项目在接入网络接口时, 有个比较值得注意的地方, 就是一个界面存在多个网络接口, 比如: 在首页界面中存在多个网络请求接口(包括: 图片轮播器, 黄金产品以及黄金样品保单). 如果按照平常那种思维来编码, 很可能会出现UI刷新不出来的问题. 造成界面空白现象. 我先说一下背景: 公司使用的网络类是对AFNetworking框架进行了再度封装, 虽然大家都对它再熟悉不过了, 但是细节上的东西还是需要慢慢品味. 本章文章主要涉及到的是GCD中几个比较常见的函数.重在基础, 大神可以忽略. 如果文章中存在问题, 希望大神在底部留言, 指导一下小白.
思路: 首页中后台提供了三个网络请求接口, 目的就是当所有的请求操作都完成之后, 才去刷新界面, 显示界面. 脑袋中一闪而过的是GCD中的队列组, 将请求操作添加队列组, 最后在dispatch_group_notify中刷新UI.
eg: (注意: 在dispatch_group_notify中打印的顺序是随机的)
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"比如说这里是在子线程上的第一个请求");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"比如说这里是子线程上的第二个请求");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"比如说这里是子线程上的第三个请求");
});
// 执行完毕之后的通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"需要在这里回到主线程刷新UI");
});
- 我们先看看打印结果, 看看结果中有没有什么猫腻
打印结果:
2017-02-15 09:22:08.287 text[987:26314] 比如说这里是子线程上的第二个请求
2017-02-15 09:22:08.287 text[987:26311] 比如说这里是在子线程上的第一个请求
2017-02-15 09:22:08.287 text[987:26312] 比如说这里是子线程上的第三个请求
2017-02-15 09:22:08.302 text[987:26273] 需要在这里回到主线程刷新UI
解释: 单纯这个例子中,很难看出什么端倪, 但是这时候需要想到在开发中使用场景是网络请求. 可能出现的情况: 在实际开发中, 我使用的AFN框架来实现网络请求, AFN中的网络请求都是异步操作, 就是说请求的数据返回后, 才会去刷新相关的UI
如果请求操作有多个, 所以必须要所有的操作都完成之后, 才去刷新UI,这样就可能会造成一个现象, 就是数据是返回了, 但是刷新后UI不显示.最后导致界面空白无物.
- 解决方法
解决方法: 根据上述的现象, 这里需要引进另一个函数
dispatch_semaphore(即: GCD中的信号量), 通过GCD中的信号量实现线程同步
dispatch_semaphore概念: 信号量是基于计数器的一种多线程同步机制, 主要是用于解决多个线程访问共有的资源时.造成数据紊乱的问题.
dispatch_semaphore基本原理: 比如说网络请求成功或者失败之后需要将dispatch_semaphore计数器 +1, 请求网络操作完成之后需要将dispatch_semaphore计数器 -1. 如果dispatch_semaphore计数器等于0表示等待.
加1操作: dispatch_semaphore_signal(semaphore)
等待操作: dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)举个例子: 每当有顾客点餐,计数+1,点餐结束-1归零继续等待下一位顾客。比较类似于NSLock(线程锁)。
eg:
- (void)request_A {
//创建信号量并设置计数默认为0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];;
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSDictionary *parameter = @{@"key":@"value"
};
[manager POST:URL parameters:parameter progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
//计数+1操作
dispatch_semaphore_signal(sema);
NSLog(@"在这里获取数据");
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
////计数+1操作
dispatch_semaphore_signal(sema);
NSLog(@"在这里获取error");
}];
//若计数为0则一直等待
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
为了便于记住, 一下是简写
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
成功:dispatch_semaphore_signal(sema);
失败:dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
// 强行解释一波
通过使用GCD中的信号量可以解决多个操作共用同一资源时, 造成主线程阻塞的问题.
知识扩展
- 扩展1: GCD中提供了函数, 可以指定操作的执行顺序
> 扩展: 如果我们要指定网络操作的执行顺序的话, 直接使用GCD中的队列, 然后添加依赖即可.
eg:
//1.任务一:
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@" //1.任务一:")
}];
//2.任务二:
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@" //1.任务二:")
}];
//3.任务三:
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@" //1.任务三:")
}];
//4.设置依赖
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二
//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
- 扩展2: 在Objctive-C中GCD提供了两种方式来支持dispatch队列的同步,就是dispatch组(队列组)和信号量(dispatch_semaphore)
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 启动队列组中的block, 然后关联到队列组group中,
队列组的block操作
@param group 队列组
@param queue#> 可以是全局的dispatch_get_global_queue(0, 0), 也可以是dispatch_get_main_queue()
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
NSLog(@"具体的网络请求操作");
});
// 超时参数
#define DISPATCH_TIME_NOW (0ull) // 现在
#define DISPATCH_TIME_FOREVER (~0ull) // 一直
// 表示: 等到group关联的block执行完毕
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 当group组执行完毕之后, 需要通知执行完毕, 那么就会使用到GCD中的dispatch_barrier_async函数
// 主要队列组中的操作执行完毕后就会调用这个函数中block
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"队列组中的操作执行完毕之后就会调用该函数");
});
// 我们也可以管理队列组的运行状态或者计数, 使用下面两个函数的时候需要注意的一点就是, 进入或者退出, 他们的次数必须要匹配.
dispatch_group_enter(group); // 进入
dispatch_group_leave(group); // 退出
// 所以,我们也可以利用dispatch_group_enter、 dispatch_group_leave和dispatch_group_wait来实现同步
- 信号量
二、dispatch信号量(dispatch semaphore)
1. 创建信号量,可以设置信号量的资源数。0表示没有资源,调用dispatch_semaphore_wait会立即等待。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
2. 等待信号,可以设置超时参数。该函数返回0表示得到通知,非0表示超时。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
3. 通知信号,如果等待线程被唤醒则返回非0,否则返回0。
dispatch_semaphore_signal(semaphore);
最后,还是回到生成消费者的例子,使用dispatch信号量是如何实现同步: