iOS 让代码实现“冷却”机制

必要性

首先我个人认为设计合理、逻辑严谨的代码是不需要强行冷却的,但是我们不能保证我们面对的代码永远都是完美的,所以我在 AXKit 中就提供了这个冷却机制以延长那些癌症晚期的代码的寿命。

  • 优点:执行代码像放技能一样,可以强行打破死循环、避免死循环、避免过高频率访问某一资源。
  • 缺点:治标不治本,最好的解决办法是找出会产生问题的代码进行重构,从根源上解决问题。

应用场景

场景1

某种耗时耗能的操作,希望在某种条件下触发,但又担心用户频繁触发,例如进入某个页面的时候同步一下设备电量、或者同步一下运动数据;进入某个页面预加载一下子页面的网络数据……就可以这样:

1
2
3
4
5
6
7
8
9
10
// @xaoxuu: 重新获取数据源并刷新tableView
- (void)reloadDataAndRefreshTableView{
// 无论如何,2秒内最多只会执行一次此方法。
ax_dispatch_cooldown(0, 2, @"reload data and refresh table view", dispatch_get_main_queue(), ^{
[self.dataList removeAllObjects];
[self reloadTableView];
}, ^{
AXLogFailure(@"操作过于频繁");
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (IBAction)btn1:(UIButton *)sender {
// @xaoxuu: 立即在主线程施放大招,冷却时间是60秒。
ax_dispatch_cooldown(0, 60, self, dispatch_get_main_queue(), ^{
AXLogSuccess(@"技能施放成功!");
}, ^{
AXLogFailure(@"抱歉,技能在冷却中");
});
}

- (IBAction)btn2:(UIButton *)sender {
// @xaoxuu: 延迟2秒后在后台默默施放大招,冷却时间是120秒。
ax_dispatch_cooldown(2, 120, self, dispatch_get_global_queue(0, 0), ^{
AXLogSuccess(@"技能施放成功!");
}, ^{
AXLogFailure(@"抱歉,技能在冷却中");
});
}
// @xaoxuu: 两者的token相同则共享冷却时间。

场景2

如何强行打破死循环?说实话我是没有遇到这种需求,仅仅是这个机制有这种能力,觉得挺有趣,就尝试一下:

1
2
3
4
5
// @xaoxuu: 自己调用自己,无限循环
- (void)loop{
AXLogFunc();
[self loop];
}

1
2
3
4
5
6
7
// @xaoxuu: 自己调用自己,但是发现代码在冷却,所以就失效了,一个环节被中断,死循环就被打破了
- (void)loop{
AXLogFunc();
ax_dispatch_cooldown(0, 0.0001, @"loop", dispatch_get_main_queue(), ^{
[self loop];
}, nil);
}

实现原理

简单地说,就是给每一个代码块分配一个dispatch_after的函数,执行的时候开始计时,并且函数标记为disable,计时结束后重新enable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
拥有冷却机制的dispatch

@param delay 延迟时间
@param cooldown 冷却时间
@param token 冷却计时的token,如果相同,则共享冷却时间
@param queue 指定线程
@param block 要执行的block
@param block_cooling 如果在冷却中要执行的block
@return 操作口令(用于取消此操作)
*/
inline ax_dispatch_operation_t ax_dispatch_cooldown(NSTimeInterval delay, NSTimeInterval cooldown,id token, dispatch_queue_t queue, void (^block)(void), void (^ __nullable block_cooling)(void)){
if (!token) {
token = @"default token";
}
if (!queue) {
queue = dispatch_get_main_queue();
}
BOOL cooling = is_cooling(token);
if (cooling) {
if (block_cooling) {
block_cooling();
}
} else {
set_is_cooling(YES, token);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(cooldown * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
set_is_cooling(NO, token);
});
return ax_dispatch_cancellable(delay, queue, block);
}
return nil;
}

其中,对于代码是否正在冷却的判断利于了runtime机制,相当于新增了一个属性,用来保存是否正在冷却的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
static const void *AXBlockWrapperKey = &AXBlockWrapperKey;

static inline BOOL is_cooling(id token){
NSNumber *cooling = objc_getAssociatedObject(token, AXBlockWrapperKey);
return cooling.boolValue;
}

static inline void set_is_cooling(BOOL cooling, id token){
if (!token) {
token = @"";
}
objc_setAssociatedObject(token, AXBlockWrapperKey, @(cooling), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}