一个对dispatch_group的改进:TaskHarmonizer

dispatch_group

dispatch_group是GCD中给我们提供的一个非常好用的功能,它的主要用途是协同任务。即保证某个任务一定在其他任务完成之后才进行。
典型的使用案例是:进入页面后,我们需要等待三个网络请求,等三个界面都返回后在主线程刷新界面(下面简称案例)
dispatch_group实现以上案例的逻辑。

dispatch_group实现案例的时候主要涉及到4个接口,简单介绍如下:
dispatch_group_create 创建一个dispatch_group_t对象
void dispatch_group_enter(dispatch_group_t group); //group里任务数递增
void dispatch_group_leave(dispatch_group_t group); //group里任务数递减
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);//设置一组任务都完成后的回调,第一个参数是要等待的group,第二个参数是block要在哪个队列中执行,第三个参数是等待执行的代码块。将group执行完后需要处理的任务放block中

项目实际

在实际项目开发过程中,我们遇到的case远比案例复杂。
复杂case1:在弱网情况下,用户可能等不及三个接口都会返回就退出了页面
复杂case2:用户可能最多愿意等待2秒,如果案例中三个接口未能在2秒内返回,用户宁愿忽略这次请求
复杂case2:三个网络请求处在三个不同的业务模块,我们无法通过block对上下文的捕获来实现三个接口返回值的汇总。
遇到上面这些复杂case的时候,dispatch_group的缺点就暴露出来了:
1、当dispatch_group_leave调用次数多于dispatch_group_enter的时候,就会发生闪退
2、dispatch_group不能保存数据。如果第一个网络请求返回后,第二第三个网络请求还没有返回,那么就会出现第一个网络请求无处存放的问题。尤其当三个请求处在三个模块中的时候,dispatch_group_notify无法获取前三个网络请求的接口。如果单独为三个接口开三个成员变量,那么代码就会显得臃肿和不优雅。
3、没有取消机制。试想:如果有三个网络请求正在进行中,用户却退出登录了,那么我们将无法取消dispatch_group_notify
4、没有超时机制。在弱网情况下,三个接口全部返回所使用的时间会比较长,而用户可能最多等待两秒就想放弃了,此时我们的dispatch_group_notify却无止境地等待接口返回。
5、调用是c风格接口,与OC语法格格不入

ACTaskHarmonizer应运而生

基于以上缺点,我自己封装了一个 ACTaskHarmonizer 类,用于解决以上问题。下面就来分析一下解决上面4个缺点的思路。

1、首先ACTaskHarmonizer是单例,全局都可以调用,瞬间解决了跨模块调用问题。
2、ACTaskHarmonizer可以创建多个Harmonizer,我们称Harmonizer为任务协调器,它专门用于协调任务的时序。我们给每个Harmonizer取一个名字,名字不可重复,用于唯一确定Harmonizer,后文统称harmonizerName。内部,我们使用字典来保存所有的Harmonizer。这个机制,保证了我们同时可以创建多个Harmonizer
3、每个Harmonizer内部都维护了一个计数器,计数器初始值可以被设置。后续每增加一个任务,计数器就加一,每完成一个任务,计数器就减一。当计数器减为0时,对外进行回调。

4、第一个接口addTask

- (BOOL)addATask:(void (^)(NSString*harmonizerName, BOOL timeout,  ACDic* _Nullable stashedData))aTask//任务
			to:(NSString*)harmonizerName//任务名称
initialCounter:(NSInteger)initialCounter//初始计数器
	mainThread:(BOOL)mainThread;//要不要在主线程中执行aTask

这个接口用于创建一个Harmonizer,它需要四个参数,后续会介绍。为了后面简单的称呼,我们称这个接口为“add接口”

第一个参数表示:当计数器减为0时的回调,后文统称notify。这个notify又有三个参数,第一个参数就是harmonizerName,第二个参数表示是否因为超时而发生了回调,第三个参数就是汇总的数据,本质上是一个字典,可以用自定义的Key值来访问到暂存的数据;
第二个参数表示:harmonizerName,我们的ACTaskHarmonizer可以创建多个任务,任务是用名称harmonizerName来区分的;
第三个参数表示:计数器初始值,上述案例中,我们需要等待三个网络都返回后才进行回调,那么我们建议初始值为3;
第四个参数表示:notify的调用需要在主队列中进行还是在系统自创的global_queue中进行,案例中我们需要在回调里刷新UI,那么建议在主队列中回调,值传YES;

5、第二个接口

- (BOOL)countUp:(NSString*)harmonizerName;//向上记数,目前线程不安全,建议在主线程调用
- (BOOL)countDown:(NSString*)harmonizerName;//向上记数,目前线程不安全,
  • 建议在主线程调用,计数器减为 0 时触发notify,然后删除整个Harmonizer条目。针对已删除的Harmonizer条目调用countDown,不会引起闪退

这两个接口用于在初始值的技术上递增或者递减计数器,考虑到大部分时候是在主线程进行,为了节省性能,这里没有考虑线程安全。如果只在主线程中调用是没有问题的。对于上述案例,我们也可以将初始值设为0,然后再每一次的任务网络请求开始前调用countUp。参数是harmonizerName

6、第三个接口

- (BOOL)stashData:(nullable id)data//暂存一些数据,在任务执行之后自动释放
		 dataName:(NSString*)dataName//寄存的数据名称
			   to:(NSString*)harmonizerName;//任务名称

用于每个网络请求返回后,将数据暂存,而dataName就表示了数据的名称,我们需要给每次的数据都取一个名字,以便于后续取出。第一个参数是任意类型。一般来说,不管请求成功或者失败,我们建议给data传递请求结果(错误码或者数据),给dataName传的递接口号字符串或者NSNumber。这里传入的数据,将会在add接口的notify里stashedData露出,用于汇总数据从而UI刷新。

7、第四个接口

- (BOOL)cancel:(NSString*)harmonizerName;//取消任务

用于取消进行中的Harmonizer。如果用户在三个接口都返回前退出了页面,我们需要取消这个harmonizerName,参数就是harmonizerName。取消后,add接口的notify将被释放,暂存的数据也被清空,整个名称为harmonizerName的harmonizer条目会被释放。取消已经被释放的harmonizer,不会引起闪退

8、第五个接口

- (BOOL)setTimeout:(NSTimeInterval)timeout to:(NSString*)harmonizerName;//设置超时

如果用户处在弱网情况下,用户可能没有耐心等待三个网络请求全部返回,用户期望2秒内没有返回就跳过UI刷新,我们可以设置一个一次性的定时器,定时器触发后将计数器强行置成0,然后触发add接口的notify回调,且timeout参数值为YES。如果timeout传负值,超时为无限

9、第六个接口

- (NSInteger)getCounter:(NSString*)harmonizerName;//获取当前记数值

这个接口用于获取harmonizer的计数器值,如果为0,表明当前的所有任务已经完成并完成了回调

调用举例

再举一个案例来说明我们的TaskHarmonizer怎么使用。我以前做的在线英语教学项目,在小朋友进入跟唱页面的时候,我需要下载三个文件,播放一段本地音频
1、上次小朋友的跟唱录音MP3文件,如果是第一次跟唱,则没有这个文件
2、标准原唱音频文件MP3
3、标准领唱音频文件MP3
4、播放本地和音频:“小朋友,请带好耳机,准备跟唱哦”,时长越3秒
以上4个任务都完成后,将拉开幕布,进入跟唱环节。

我们在这里用伪代码表示一下进入跟唱页的步骤。

		NSString* harmonizerName = @"进入跟唱页面harmonizerName";//名称定义,VC的成员变量


///在viewDidLoad里调用
		[ACTaskHarmonizer.sharedHarmonizer addATask:^(NSString * _Nonnull harmonizerName, BOOL timeout, NSDictionary<NSString *,id> * _Nullable stashedData) {
   
			if (timeout)
{
   
//下载超时,退出页面
}
else
{
   
//取出任务形成的数据
NSString* localUrl上次小朋友的跟唱录音MP3文件 = stashedData[@"上次小朋友的跟唱录音MP3文件"];
NSString* localUrl标准原唱音频文件MP3 = stashedData[@"标准原唱音频文件MP3"
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值