iOS 中的 防抖(debounce) 与 节流(throttle)
什么是防抖与节流
函数防抖(debounce) 是指在一定时间内,在动作被连续频繁触发的情况下,动作只会被执行一次,也就是说当调用动作过 n 毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间,所以短时间内的连续动作永远只会触发一次。 函数节流(throttle) 是指一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期,一个比较形象的例子是如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。
regular 代表常规情况,不做限制时,函数直接调用的结果。 deboundce 代表防抖,可以发现,如果函数一直调用,它不会立即执行,而是等到一段时间后,函数没有新调用,它才执行一次。 throttle 代表节流,在一定时间内,只执行一次
为什么要用防抖与节流
开发中我们都遇到频率很高的事件(如搜索框的搜索)或者连续事件(如UIScrollView的contentOffset进行某些计算),这个时候为了进行性能优化就要用到Throttle和Debounce。
防抖(debounce)
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- 列表刷新,为避免短时间内反复 reload,可以多次合并为一次
- TCP 流量控制
节流(throttle)
- 防止多次点击
- 重复发多个网络请求
iOS 实践
防抖(debounce)
/// 延时执行
DispatchQueue(label: "Debounce").asyncAfter(deadline: .now() + DispatchTimeInterval.milliseconds(2000), execute: DispatchWorkItem {
print("Debounce -----------")
})
节流(throttle)
let item = DispatchWorkItem { [weak self] in
self?.lastTime = Date()
print("Throttle -----------")
}
let new = Date()
let delay = new.timeIntervalSince1970 - lastTime.timeIntervalSince1970 > Double(2000)/1000 ? DispatchTimeInterval.milliseconds(0) : DispatchTimeInterval.milliseconds(2000)
DispatchQueue(label: "Throttle").asyncAfter(deadline: .now() + delay, execute: item)
轮子 MessageThrottle
其实在 iOS 中有现成的轮子可以用,那就是 MessageThrottle,使用方法: 假如我创建了一个 Stub 类的实例 s,我想限制它调用 foo: 方法的频率。先要创建并配置一个 MTRule,并将规则应用到 MTEngine 单例中:
Stub *s = [Stub new];
MTRule *rule = [MTRule new];
rule.target = s; // You can also assign `Stub.class` or `mt_metaClass(Stub.class)`
rule.selector = @selector(foo:);
rule.durationThreshold = 0.01;
[MTEngine.defaultEngine applyRule:rule]; // or use `[rule apply]`
当然还有更简单的用法,跟上面那段代码作用相同:
[s limitSelector:@selector(foo:) oncePerDuration:0.01];
主要就是对 MTRule 的设置,来决定我们将以哪种模式,多少的时间限制来控制方法调用。
MessageThrottle原理
主要思路是:对进行节流和防抖的方法,利用运行时特性进行 hook,然后再统一做处理,我这里简单介绍一下
- 给类添加一个新的方法 fixed_selector,对应实现为 rule.selector 的 IMP。
- 利用 Objective-C runtime 消息转发机制,将 rule.selector 对应的 IMP 改成 _objc_msgForward 从而触发调用 forwardInvocation: 方法。
- 将 forwardInvocation: 的实现替换为自己实现的 IMP,并在自己实现的逻辑中将 invocation.selector 设为 fixed_selector。并限制 [invocation invoke] 的调用频率。
核心代码
/**
处理执行 NSInvocation
@param invocation NSInvocation 对象
@param rule MTRule 对象
*/
static void mt_handleInvocation(NSInvocation *invocation, MTRule *rule)
{
NSCParameterAssert(invocation);
NSCParameterAssert(rule);
if (!rule.isActive) {//规则非 active 状态的,直接 invoke
[invocation invoke];
return;
}
if (rule.durationThreshold <= 0 || mt_invokeFilterBlock(rule, invocation)) {//时间小于等于0,设置aliasSelector(为原始方法IMP)后执行.
invocation.selector = rule.aliasSelector;
[invocation invoke];
return;
}
//时间戳处理,用 correctionForSystemTime 校正系统时间所需的差值。
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
now += MTEngine.defaultEngine.correctionForSystemTime;
switch (rule.mode) {
//节流模式:执行第一次触发
case MTPerformModeFirstly: {
//触发时,直接看现在的时间间隔是否比限制时间大,如果大于则直接执行,否则不响应
if (now - rule.lastTimeRequest > rule.durationThreshold) {
invocation.selector = rule.aliasSelector;
[invocation invoke];
//执行后,更新最近执行时间
rule.lastTimeRequest = now;
dispatch_async(rule.messageQueue, ^{
// May switch from other modes, set nil just in case.
rule.lastInvocation = nil;
});
}
break;
}
//节流模式:执行最后一次触发
case MTPerformModeLast: {
invocation.selector = rule.aliasSelector;
//invocation 提前持有参数,防止延迟执行时被释放掉
[invocation retainArguments];
dispatch_async(rule.messageQueue, ^{
//更新最近触发的 invocation
rule.lastInvocation = invocation;
//如间隔时间超出 rule 限定时间,则对方法做执行。保证为最后一次调用
if (now - rule.lastTimeRequest > rule.durationThreshold) {
//更新执行时间
rule.lastTimeRequest = now;
//按规则的间隔时间后执行 invoke
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
if (!rule.isActive) {
rule.lastInvocation.selector = rule.selector;
}
[rule.lastInvocation invoke];
//invoke 后将 lastInvocation 置 nil
rule.lastInvocation = nil;
});
}
});
break;
}
//防抖模式:一段时间内不再有新触发,再执行
case MTPerformModeDebounce: {
//设置 invocation 的 selector
invocation.selector = rule.aliasSelector;
//提前持有参数
[invocation retainArguments];
dispatch_async(rule.messageQueue, ^{
//更新 invocation
rule.lastInvocation = invocation;
//在限制时间段过后做执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
//假如还是rule.invocation 和 invocation一样,证明没有新的触发,达到执行条件
if (rule.lastInvocation == invocation) {
if (!rule.isActive) {
rule.lastInvocation.selector = rule.selector;
}
[rule.lastInvocation invoke];
rule.lastInvocation = nil;
}
});
});
break;
}
}
}
具体实现可以去看源码,源码里有作者的详细解释,是否选择使用要看喜好与业务需求了~