函数节流(Throttle)和防抖(Debounce)解析及其iOS实现

1、Throttle和Debounce是什么

Throttle本是机械领域的概念,英文解释为:前端

A valve that regulates the supply of fuel to the engine.git

中文翻译成节流器,用以调节发动机燃料供应的阀门。在计算机领域,一样也引入了Throttle和Debounce概念,这两种技术均可用来下降函数调用频率,类似又有区别。对于连续调用的函数,尤为是触发频率密集、目标函数涉及大量计算时,恰当使用Throttle和Debounce能够有效提高性能及系统稳定性。github

对于JS前端开发人员,因为没法控制DOM事件触发频率,在给DOM绑定事件的时候,经常须要进行Throttle或者Debounce来防止事件调用过于频繁。而对于iOS开发者来讲,也许会以为这两个术语很陌生,不过你极可能在不经意间已经用到了,只是没想过会有专门的抽象概念。举个常见的例子,对于UITableView,频繁触发reloadData函数可能会引发画面闪动、卡顿,数据源动态变化时甚至会致使崩溃,一些开发者可能会千方百计减小对reload函数的调用,不过对于复杂的UITableView视图可能会显得捉襟见肘,由于reloadData极可能“无处不在”,甚至会被跨文件调用,此时就能够考虑对reloadData函数自己作降低频处理。编程

下面经过概念定义及示例来详细解析对比下Throttle和Debounce,先看下两者在JS的Lodash库中的解释:markdown

Throttle

Throttle enforces a maximum number of times a function can be called over time. For example, "execute this function at most once every 100 ms."网络

即,Throttle使得函数在规定时间间隔内(如100 ms),最多只能调用一次。app

Debounce

Debounce enforces that a function not be called again until a certain amount of time has passed without it being called. For example, "execute this function only if 100 ms have passed without it being called."async

即,Debounce能够将小于规定时间间隔(如100 ms)内的函数调用,归并成一次函数调用。函数

对于Debounce的理解,能够想象一下电梯的例子。你在电梯中,门快要关了,忽然又有人要进来,电梯此时会再次打开门,直到短期内没有人再进为止。虽然电梯上行下行的时间延迟了,可是优化了总体资源配置。oop

咱们再以拖拽手势回调的动图展现为例,来直观感觉下Throttle和Debounce的区别。每次“walk me”图标拖拽时,会产生一次回调。在动图的右上角,能够看到回调函数实际调用的次数。

1)正常回调:

正常回调

2)Throttle(Leading)模式下的回调:

Throttle(Leading)模式下的回调

3)Debounce(Trailing)模式下的回调:

Debounce(Trailing)模式下的回调

2、应用场景

如下是几个典型的Throttle和Debounce应用场景。

1)防止按钮重复点击

为了防止用户重复快速点击,致使冗余的网络请求、动画跳转等没必要要的损耗,可使用Throttle的Leading模式,只响应指定时间间隔内的第一次点击。

2)滚动拖拽等密集事件

能够在UIScrollView的滚动回调didScroll函数里打日志观察下,调用频率至关高,几乎每移动1个像素均可能产生一次回调,若是回调函数的计算量偏大极可能会致使卡顿,此种状况下就能够考虑使用Throttle降频。

3)文本输入自动完成

假如想要实现,在用户输入时实时展现搜索结果,常规的作法是用户每改变一个字符,就触发一次搜索,但此时用户极可能尚未输入完成,形成资源浪费。此时就可使用Debounce的Trailing模式,在字符改变以后的一段时间内,用户没有继续输入时,再触发搜索动做,从而有效节省网络请求次数。

4)数据同步

以用户埋点日志上传为例,不必在用户每操做一次后就触发一次网络请求,此时就可使用Debounce的Traling模式,在记录用户开始操做以后,且一段时间内再也不操做时,再把日志merge以后上传至服务端。其余相似的场景,好比客户端与服务端版本同步,也能够采起这种策略。

在系统层面,或者一些知名的开源库里,也常常能够看到Throttle或者Debounce的身影。

5) GCD Background Queue

Items dispatched to the queue run at background priority; the queue is scheduled for execution after all high priority queues have been scheduled and the system runs items on a thread whose priority is set for background status. Such a thread has the lowest priority and any disk I/O is throttled to minimize the impact on the system.

在dispatch的Background Queue优先级下,系统会自动将磁盘I/O操做进行Throttle,来下降对系统资源的耗费。

6)ASIHttpRequest及AFNetworking

- (void)handleNetworkEvent:(CFStreamEventType)type
{
    //...
    [self performThrottling];
    //...
}
复制代码
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
                                  delay:(NSTimeInterval)delay;
复制代码

在弱网环境下, 一个Packet一次传输失败的几率会升高,因为TCP是有序且可靠的,前一个Packet不被ack的状况下,后面的Packet就要等待,因此此时若是启用Network Throttle机制,减少写入数据量,反而会提高网络请求的成功率。

3、iOS实现

理解了Throttle和Debounce的概念后,在单个业务场景中实现起来是很容易的事情,可是考虑到其应用如此普遍,就应该封装成为业务无关的组件,减少重复劳动,提高开发效率。

前文提过,Throttle和Debounce在Web前端已经有至关成熟的实现,Ben Alman以前作过一个JQuery插件(再也不维护),一年后Jeremy Ashkenas把它加入了underscore.js,然后又加入了Lodash。可是在iOS开发领域,尤为是对于Objective-C语言,尚且没有一个可靠、稳定且全面的第三方库。

杨萧玉曾经开源过一个MessageThrottle库,该库使用Objective-C的runtime与消息转发机制,使用很是便捷。可是这个库的缺点也比较明显,使用了大量的底层HOOK方法,系统兼容性方面还须要进一步的验证和测试,若是集成的项目中同时使用了其余使用底层runtime的方法,可能会产生冲突,致使非预期后果。另外该库是彻底面向切面的,做用于全局且隐藏较深,增长了必定的调试成本。 为此笔者封装了一个新的实现HWThrottle,并借鉴了Lodash的接口及实现方式,该库有如下特色:

1)未使用任何runtime API,所有由顶层API实现;

2)每一个业务场景须要使用者本身定义一个实例对象,自行管理生命周期,旨在把对项目的影响控制在最小范围;

3)区分Throttle和Debounce,提供Leading和Trailing选项。

Demo

下面展现了对按钮点击事件进行Throttle或Debounce的效果,click count表示点击按钮次数,call count表示实际调用目标事件的次数。

在leading模式下,会在指定时间间隔的开始处触发调用;Trailing模式下,会在指定时间间隔的末尾处触发调用。

1) Throttle Leading

Throttle Leading

2) Throttle Trailing

Throttle Trailing

3) Debounce Trailing

Debounce Trailing

4) Debounce Leading

Debounce Leading

使用示例:

if (!self.testThrottler) {
        self.testThrottler = [[HWThrottle alloc] initWithThrottleMode:HWThrottleModeLeading
                                                                   interval:1
                                                                    onQueue:dispatch_get_main_queue()
                                                                  taskBlock:^{
           //do some heavy tasks
        }];
    }
    [self.testThrottler call];
复制代码

因为使用到了block,注意在Throttle或Debounce对象全部者即将释放时,即再也不使用block时调用invalidate,该方法会将持有的task block置空,防止循环引用。若是是在页面中使用Throttle或Debounce对象,可在disappear回调中调用invalidate方法。

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.testThrottler invalidate];
}
复制代码

接口API:

HWThrottle.h:

#pragma mark - public class

typedef NS_ENUM(NSUInteger, HWThrottleMode) {
    HWThrottleModeLeading,          //invoking on the leading edge of the timeout
    HWThrottleModeTrailing,         //invoking on the trailing edge of the timeout
};

typedef void(^HWThrottleTaskBlock)(void);

@interface HWThrottle : NSObject

/// Initialize a throttle object, the throttle mode is the default HWThrottleModeLeading, the execution queue defaults to the main queue. Note that throttle is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other
/// @param interval Throttle time interval, unit second
/// @param taskBlock The task to be throttled
- (instancetype)initWithInterval:(NSTimeInterval)interval
                       taskBlock:(HWThrottleTaskBlock)taskBlock;

/// Initialize a throttle object, the throttle mode is the default HWThrottleModeLeading. Note that throttle is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other
/// @param interval Throttle time interval, unit second
/// @param queue Execution queue, defaults the main queue
/// @param taskBlock The task to be throttled
- (instancetype)initWithInterval:(NSTimeInterval)interval
                         onQueue:(dispatch_queue_t)queue
                       taskBlock:(HWThrottleTaskBlock)taskBlock;

/// Initialize a debounce object. Note that debounce is for the same HWThrottle object, and different HWThrottle objects do not interfere with each other
/// @param throttleMode The throttle mode, defaults HWThrottleModeLeading
/// @param interval Throttle time interval, unit second
/// @param queue Execution queue, defaults the main queue
/// @param taskBlock The task to be throttled
- (instancetype)initWithThrottleMode:(HWThrottleMode)throttleMode
                            interval:(NSTimeInterval)interval
                             onQueue:(dispatch_queue_t)queue
                           taskBlock:(HWThrottleTaskBlock)taskBlock;


/// throttling call the task
- (void)call;


/// When the owner of the HWThrottle object is about to release, call this method on the HWThrottle object first to prevent circular references
- (void)invalidate;

@end
复制代码

Throttle默认模式为Leading,由于实际使用中,多数的Throttle场景是在指定时间间隔的开始处调用,好比防止按钮重复点击时,通常会响应第一次点击,而忽略以后的点击。

HWDebounce.h:

#pragma mark - public class

typedef NS_ENUM(NSUInteger, HWDebounceMode) {
    HWDebounceModeTrailing,        //invoking on the trailing edge of the timeout
    HWDebounceModeLeading,         //invoking on the leading edge of the timeout
};

typedef void(^HWDebounceTaskBlock)(void);

@interface HWDebounce : NSObject

/// Initialize a debounce object, the debounce mode is the default HWDebounceModeTrailing, the execution queue defaults to the main queue. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other
/// @param interval Debounce time interval, unit second
/// @param taskBlock The task to be debounced
- (instancetype)initWithInterval:(NSTimeInterval)interval
                       taskBlock:(HWDebounceTaskBlock)taskBlock;

/// Initialize a debounce object, the debounce mode is the default HWDebounceModeTrailing. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other
/// @param interval Debounce time interval, unit second
/// @param queue Execution queue, defaults the main queue
/// @param taskBlock The task to be debounced
- (instancetype)initWithInterval:(NSTimeInterval)interval
                         onQueue:(dispatch_queue_t)queue
                       taskBlock:(HWDebounceTaskBlock)taskBlock;

/// Initialize a debounce object. Note that debounce is for the same HWDebounce object, and different HWDebounce objects do not interfere with each other
/// @param debounceMode The debounce mode, defaults HWDebounceModeTrailing
/// @param interval Debounce time interval, unit second
/// @param queue Execution queue, defaults the main queue
/// @param taskBlock The task to be debounced
- (instancetype)initWithDebounceMode:(HWDebounceMode)debounceMode
                            interval:(NSTimeInterval)interval
                             onQueue:(dispatch_queue_t)queue
                           taskBlock:(HWDebounceTaskBlock)taskBlock;


/// debouncing call the task
- (void)call;


/// When the owner of the HWDebounce object is about to release, call this method on the HWDebounce object first to prevent circular references
- (void)invalidate;

@end
复制代码

Debounce默认模式为Trailing,由于实际使用中,多数的Debounce场景是在指定时间间隔的末尾处调用,好比监听用户输入时,通常是在用户中止输入后再触发调用。

核心代码:

Throttle leading:

- (void)call {
    if (self.lastRunTaskDate) {
        if ([[NSDate date] timeIntervalSinceDate:self.lastRunTaskDate] > self.interval) {
            [self runTaskDirectly];
        }
    } else {
        [self runTaskDirectly];
    }
}

- (void)runTaskDirectly {
    dispatch_async(self.queue, ^{
        if (self.taskBlock) {
            self.taskBlock();
        }
        self.lastRunTaskDate = [NSDate date];
    });
}

- (void)invalidate {
    self.taskBlock = nil;
}

复制代码

Throttle trailing:

- (void)call {
    NSDate *now = [NSDate date];
    if (!self.nextRunTaskDate) {
        if (self.lastRunTaskDate) {
            if ([now timeIntervalSinceDate:self.lastRunTaskDate] > self.interval) {
                self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:now];
            } else {
                self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:self.lastRunTaskDate];
            }
        } else {
            self.nextRunTaskDate = [NSDate dateWithTimeInterval:self.interval sinceDate:now];
        }
        
        
        NSTimeInterval nextInterval = [self.nextRunTaskDate timeIntervalSinceDate:now];
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(nextInterval * NSEC_PER_SEC)), self.queue, ^{
            if (self.taskBlock) {
                self.taskBlock();
            }
            self.lastRunTaskDate = [NSDate date];
            self.nextRunTaskDate = nil;
        });
    }
}

- (void)invalidate {
    self.taskBlock = nil;
}

复制代码

Debounce trailing:

- (void)call {
    if (self.block) {
        dispatch_block_cancel(self.block);
    }
    __weak typeof(self)weakSelf = self;
    self.block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{
        if (weakSelf.taskBlock) {
            weakSelf.taskBlock();
        }
    });
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.interval * NSEC_PER_SEC)), self.queue, self.block);
}

- (void)invalidate {
    self.taskBlock = nil;
    self.block = nil;
}

复制代码

Debounce leading:

- (void)call {
    if (self.lastCallTaskDate) {
        if ([[NSDate date] timeIntervalSinceDate:self.lastCallTaskDate] > self.interval) {
            [self runTaskDirectly];
        }
    } else {
        [self runTaskDirectly];
    }
    self.lastCallTaskDate = [NSDate date];
}

- (void)runTaskDirectly {
    dispatch_async(self.queue, ^{
        if (self.taskBlock) {
            self.taskBlock();
        }
    });
}

- (void)invalidate {
    self.taskBlock = nil;
    self.block = nil;
}

复制代码

4、总结

但愿此篇文章能帮助你全面理解Throttle和Debounce的概念,赶快看看项目中有哪些能够用到Throttle或Debounce来提高性能的地方吧。

再次附上OC实现HWThrottle,欢迎issue和讨论。

5、参考文章

[1]iOS编程中throttle那些事

[2]Objective-C Message Throttle and Debounce

[3]Lodash Documentation