在最近工作中,由于种种原因,经常碰到这样一个需求:在显示一个界面之前,需要同时从多个接口获取数据,对全部的数据进行综合处理后才能显示界面。感谢后台大佬和产品( ),下面谈谈我实现的思路与方法。(其实是当笔记:-D)
总体思路
界面上效果:进入控制器后,进行网络请求,界面显示未转菊花动画,等数据全部获取并处理后,隐藏菊花,显示正确界面。
问题简化:其实就是一个多线程问题,不管需要同时请求几个接口,这些通过访问接口获取数据的方法都是需要一定时间的。对于这种耗时操作,我们通常需要开辟子线程去处理,在子线程中进行等待,阻塞,主线程只要负责界面显示,在这个例子中是显示菊花,告诉用户我们正在处理数据。等子线程处理完所有数据后,再通知主线程绘制界面。那么最终的问题就是准备如何知道所有的异步网络请求都已经完成,即什么时候数据处理完毕。
技术实现
方法一:
1.使用GCD dispatch_group_async来作为多个异步网络请求任务的“容器”,并在dispatch_group_notify中对返回结果进行处理。
2.使用信号量来控制异步网络请求完成后等待所有数据请求回来统一处理。
demo
// 创建一个信号量对象
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_group_t group = dispatch_group_create();
//系统给定义的全局队列,我希望每个请求接口任务都能并发进行,所以选择并行队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 将请求接口任务添加进group中,并传入block方法,在数据返回并处理完毕以后,执行dispatch_semaphore_signal(semaphore);方法,且是异步操作。
// 请求红包数据
dispatch_group_async(group, queue, ^{
[self configCouponData:^{
dispatch_semaphore_signal(semaphore);
}];
});
// 请求商品详情数据
dispatch_group_async(group, queue, ^{
[self configGoodDetailData:^{
dispatch_semaphore_signal(semaphore);
}];
});
// dispatch_group_notify里面的任务是在group里面的所有任务完成后才进行。并且是异步的!!!
dispatch_group_notify(group, queue, ^{
// 等待信号量资源 如果网络请求未返回,则该线程阻塞
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 此时所有接口的数据都已经返回并处理完毕.
dispatch_async(dispatch_get_main_queue(), ^{
// 综合处理所有返回信息并更新UI
[self finishGetAllData];
});
});
方法二:
自定义一个变量当做标签,每个接口获取数据后去判断这个标签,当标签显示已经全部数据处理完时,再进行更新。这个方法最主要的是要对标签进行加锁,保障这个标签是线程安全的。我是用dispatch_barrier_async给标签加一个读写锁。
demo
/// 设置读写锁用的自定义并行队列,注意,必须得是自定义的并行队列才会有效果。
@property (nonatomic,strong)dispatch_queue_t queue;
/// 判断是否数据处理完毕的标签
@property (nonatomic,assign)NSInteger countFlag;
// 将countFlag的赋值进行dispatch_barrier_async处理
- (void)setCountFlag:(NSInteger)countFlag{
dispatch_barrier_async(self.queue, ^{
_countFlag = countFlag;
});
}
// 懒加载创建一个自定义并行队列
- (dispatch_queue_t)queue{
if (!_queue) {
_queue = dispatch_queue_create("read_write_lock", DISPATCH_QUEUE_CONCURRENT);
}
return _queue;
}
- (void)configData{
// countFlag设置的值为请求接口数+1,每返回一次接口数据,countFlag--,当countFlag == 1时,说明接口已经返回完毕。只所以不以0为标志,是因为如果网络请求回调在界面销毁以后执行,那么self.countFlag == 0,这样就可以不用再进行绘制界面处理。
self.countFlag = 3;
[self configCouponData:^{
weakSelf.countFlag --;
// 判断是否所有接口请求完毕
if (weakSelf.countFlag == 1) {
dispatch_async(dispatch_get_main_queue(), ^{
// 综合处理所有返回信息并更新UI
[weakSelf finishGetAllData];
});
}
}];
[self configCouponData:^{
weakSelf.countFlag --;
// 判断是否所有接口请求完毕
if (weakSelf.countFlag == 1) {
dispatch_async(dispatch_get_main_queue(), ^{
// 综合处理所有返回信息并更新UI
[weakSelf finishGetAllData];
});
}
}];
}
//读的锁
- (NSInteger)countFlag
{
__block NSInteger tempCountFlag;
dispatch_sync(self.queue, ^{
tempCountFlag = self.countFlag;
});
return tempCountFlag;
}
具体见利用dispatch_barrier_async设置读写锁
总结:
最终我在项目中用的是方法一,经过几个版本,错误日志中也没发现什么问题。相对来说,我比较喜欢用信号量以及这种代码风格。其实这个方法就是将dispatch_group_notify任务执行的子线程阻塞。是把每个请求数据当做一个资源,这个例子中需要同时2个接口获取数据,就需要2个资源。先创建一个信号量,给0个资源,在dispatch_group_notify任务执行的子线程中会阻塞,等待资源。而每完成一个异步网络请求,就调用一下dispatch_semaphore_signal(semaphore);增加一个资源,等全部网络请求都完成了,进行下一步界面布局的资源就齐全了,dispatch_group_notify中的任务就会继续下去。
甚至还可以给获取全部数据一个最大等待时间,超过了我们就当接口超时处理,只要把
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);设置为不永久等待,判断返回值即可。
ps:
期间,有纠结过dispatch_group_notify里面的任务是否是异步执行的,其实苹果在方法描述中已经告诉我们。
这是关于dispatch_group_t的描述,谷歌翻译下,第一句话就说了队列中加入的块内容是异步执行的。
A group of block objects submitted to a queue for asynchronous invocation.
A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.
Note that the blocks in a group may be run on different queues, and each individual block can add more blocks to the group.
The dispatch group keeps track of how many blocks are outstanding, and GCD retains the group until all its associated blocks complete execution.
而dispatch_group_notify说明里面的block是加入到group中的,前面说group中的内容是异步执行的,所以dispatch_group_notify里面应该也是异步执行的。
Schedules a block object to be submitted to a queue when a group of previously submitted block objects have completed.
This function schedules a notification block to be submitted to the specified queue when all blocks associated with the dispatch group have completed. If the group is empty (no block objects are associated with the dispatch group), the notification block object is submitted immediately.
When the notification block is submitted, the group is empty. The group can either be released with [dispatch_release](apple-reference-documentation://hccKsBdGNk) or be reused for additional block objects. See [dispatch_group_async](apple-reference-documentation://hcnC1onlYa) for more information.
并且,在oc中,同步操作dispatch_sync的方法是这样的:
而异步操作和dispatch_group_notify是这样的
oc在同步操作的block前面,加了noescape,说明该block是非逃逸闭包,即block的作用域不会超过函数本身。
而异步操作和dispatch_group_notify的block前面没有注明,是默认为逃逸闭包,即block的作用域会超过函数本身。
而同步操作,block的作用域不需要超过函数的作用域,它肯定在函数结束前回调结束,所以,会在同步操作的block前加noescape,使编译器对代码优化性能。
而在swift中也是这样的,只不过swift和oc相反,swift3.0以上是默认block回调是非逃逸的,而对于逃逸闭包需要加上关键字@escaping。