函數(shù)節(jié)流(Throttle)和防抖(Debounce)解析及其OC實(shí)現(xiàn)
作者丨highway
來(lái)源丨iOS成長(zhǎng)之路

Throttle和Debounce是什么
Throttle本是機(jī)械領(lǐng)域的概念,英文解釋為:
A valve that regulates the supply of fuel to the engine.
中文翻譯成節(jié)流器,用以調(diào)節(jié)發(fā)動(dòng)機(jī)燃料供應(yīng)的閥門。在計(jì)算機(jī)領(lǐng)域,同樣也引入了Throttle和Debounce概念,這兩種技術(shù)都可用來(lái)降低函數(shù)調(diào)用頻率,相似又有區(qū)別。對(duì)于連續(xù)調(diào)用的函數(shù),尤其是觸發(fā)頻率密集、目標(biāo)函數(shù)涉及大量計(jì)算時(shí),恰當(dāng)使用Throttle和Debounce可以有效提升性能及系統(tǒng)穩(wěn)定性。
對(duì)于JS前端開(kāi)發(fā)人員,由于無(wú)法控制DOM事件觸發(fā)頻率,在給DOM綁定事件的時(shí)候,常常需要進(jìn)行Throttle或者Debounce來(lái)防止事件調(diào)用過(guò)于頻繁。而對(duì)于iOS開(kāi)發(fā)者來(lái)說(shuō),也許會(huì)覺(jué)得這兩個(gè)術(shù)語(yǔ)很陌生,不過(guò)你很可能在不經(jīng)意間已經(jīng)用到了,只是沒(méi)想過(guò)會(huì)有專門的抽象概念。舉個(gè)常見(jiàn)的例子,對(duì)于UITableView,頻繁觸發(fā)reloadData函數(shù)可能會(huì)引起畫(huà)面閃動(dòng)、卡頓,數(shù)據(jù)源動(dòng)態(tài)變化時(shí)甚至?xí)?dǎo)致崩潰,一些開(kāi)發(fā)者可能會(huì)想方設(shè)法減少對(duì)reload函數(shù)的調(diào)用,不過(guò)對(duì)于復(fù)雜的UITableView視圖可能會(huì)顯得捉襟見(jiàn)肘,因?yàn)閞eloadData很可能“無(wú)處不在”,甚至?xí)豢缥募{(diào)用,此時(shí)就可以考慮對(duì)reloadData函數(shù)本身做下降頻處理。
下面通過(guò)概念定義及示例來(lái)詳細(xì)解析對(duì)比下Throttle和Debounce,先看下二者在JS的Lodash庫(kù)中的解釋:
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使得函數(shù)在規(guī)定時(shí)間間隔內(nèi)(如100 ms),最多只能調(diào)用一次。
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."
即,Debounce可以將小于規(guī)定時(shí)間間隔(如100 ms)內(nèi)的函數(shù)調(diào)用,歸并成一次函數(shù)調(diào)用。
對(duì)于Debounce的理解,可以想象一下電梯的例子。你在電梯中,門快要關(guān)了,突然又有人要進(jìn)來(lái),電梯此時(shí)會(huì)再次打開(kāi)門,直到短時(shí)間內(nèi)沒(méi)有人再進(jìn)為止。雖然電梯上行下行的時(shí)間延遲了,但是優(yōu)化了整體資源配置。
我們?cè)僖酝献謩?shì)回調(diào)的動(dòng)圖展示為例,來(lái)直觀感受下Throttle和Debounce的區(qū)別。每次“walk me”圖標(biāo)拖拽時(shí),會(huì)產(chǎn)生一次回調(diào)。在動(dòng)圖的右上角,可以看到回調(diào)函數(shù)實(shí)際調(diào)用的次數(shù)。
概念演示
1)正常回調(diào):

2)Throttle(Leading)模式下的回調(diào):

3)Debounce(Trailing)模式下的回調(diào):

應(yīng)用場(chǎng)景
以下是幾個(gè)典型的Throttle和Debounce應(yīng)用場(chǎng)景。
防止按鈕重復(fù)點(diǎn)擊
為了防止用戶重復(fù)快速點(diǎn)擊,導(dǎo)致冗余的網(wǎng)絡(luò)請(qǐng)求、動(dòng)畫(huà)跳轉(zhuǎn)等不必要的損耗,可以使用Throttle的Leading模式,只響應(yīng)指定時(shí)間間隔內(nèi)的第一次點(diǎn)擊。
滾動(dòng)拖拽等密集事件
可以在UIScrollView的滾動(dòng)回調(diào)didScroll函數(shù)里打日志觀察下,調(diào)用頻率相當(dāng)高,幾乎每移動(dòng)1個(gè)像素都可能產(chǎn)生一次回調(diào),如果回調(diào)函數(shù)的計(jì)算量偏大很可能會(huì)導(dǎo)致卡頓,此種情況下就可以考慮使用Throttle降頻。
文本輸入自動(dòng)完成
假如想要實(shí)現(xiàn),在用戶輸入時(shí)實(shí)時(shí)展示搜索結(jié)果,常規(guī)的做法是用戶每改變一個(gè)字符,就觸發(fā)一次搜索,但此時(shí)用戶很可能還沒(méi)有輸入完成,造成資源浪費(fèi)。此時(shí)就可以使用Debounce的Trailing模式,在字符改變之后的一段時(shí)間內(nèi),用戶沒(méi)有繼續(xù)輸入時(shí),再觸發(fā)搜索動(dòng)作,從而有效節(jié)省網(wǎng)絡(luò)請(qǐng)求次數(shù)。
數(shù)據(jù)同步
以用戶埋點(diǎn)日志上傳為例,沒(méi)必要在用戶每操作一次后就觸發(fā)一次網(wǎng)絡(luò)請(qǐng)求,此時(shí)就可以使用Debounce的Traling模式,在記錄用戶開(kāi)始操作之后,且一段時(shí)間內(nèi)不再操作時(shí),再把日志merge之后上傳至服務(wù)端。其他類似的場(chǎng)景,比如客戶端與服務(wù)端版本同步,也可以采取這種策略。
在系統(tǒng)層面,或者一些知名的開(kāi)源庫(kù)里,也經(jīng)常可以看到Throttle或者Debounce的身影。
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優(yōu)先級(jí)下,系統(tǒng)會(huì)自動(dòng)將磁盤I/O操作進(jìn)行Throttle,來(lái)降低對(duì)系統(tǒng)資源的耗費(fèi)。
ASIHttpRequest及AFNetworking
- (void)handleNetworkEvent:(CFStreamEventType)type
{
//...
[self performThrottling];
//...
}
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay;
在弱網(wǎng)環(huán)境下, 一個(gè)Packet一次傳輸失敗的概率會(huì)升高,由于TCP是有序且可靠的,前一個(gè)Packet不被ack的情況下,后面的Packet就要等待,所以此時(shí)如果啟用Network Throttle機(jī)制,減小寫(xiě)入數(shù)據(jù)量,反而會(huì)提升網(wǎng)絡(luò)請(qǐng)求的成功率。
iOS實(shí)現(xiàn)
理解了Throttle和Debounce的概念后,在單個(gè)業(yè)務(wù)場(chǎng)景中實(shí)現(xiàn)起來(lái)是很容易的事情,但是考慮到其應(yīng)用如此廣泛,就應(yīng)該封裝成為業(yè)務(wù)無(wú)關(guān)的組件,減小重復(fù)勞動(dòng),提升開(kāi)發(fā)效率。
前文提過(guò),Throttle和Debounce在Web前端已經(jīng)有相當(dāng)成熟的實(shí)現(xiàn),Ben Alman之前做過(guò)一個(gè)JQuery插件(不再維護(hù)),一年后Jeremy Ashkenas把它加入了underscore.js,而后又加入了Lodash[1]。但是在iOS開(kāi)發(fā)領(lǐng)域,尤其是對(duì)于Objective-C語(yǔ)言,尚且沒(méi)有一個(gè)可靠、穩(wěn)定且全面的第三方庫(kù)。
楊蕭玉曾經(jīng)開(kāi)源過(guò)一個(gè) MessageThrottle[2] 庫(kù),該庫(kù)使用Objective-C的runtime與消息轉(zhuǎn)發(fā)機(jī)制,使用非常便捷。但是這個(gè)庫(kù)的缺點(diǎn)也比較明顯,使用了大量的底層HOOK方法,系統(tǒng)兼容性方面還需要進(jìn)一步的驗(yàn)證和測(cè)試,如果集成的項(xiàng)目中同時(shí)使用了其他使用底層runtime的方法,可能會(huì)產(chǎn)生沖突,導(dǎo)致非預(yù)期后果。另外該庫(kù)是完全面向切面的,作用于全局且隱藏較深,增加了一定的調(diào)試成本。為此筆者封裝了一個(gè)新的實(shí)現(xiàn)HWThrottle,并借鑒了Lodash的接口及實(shí)現(xiàn)方式,該庫(kù)有以下特點(diǎn):
1、未使用任何runtime API,全部由頂層API實(shí)現(xiàn);
2、每個(gè)業(yè)務(wù)場(chǎng)景需要使用者自己定義一個(gè)實(shí)例對(duì)象,自行管理生命周期,旨在把對(duì)項(xiàng)目的影響控制在最小范圍;
3、區(qū)分Throttle和Debounce,提供Leading和Trailing選項(xiàng)。
Demo
下面展示了對(duì)按鈕點(diǎn)擊事件進(jìn)行Throttle或Debounce的效果,click count表示點(diǎn)擊按鈕次數(shù),call count表示實(shí)際調(diào)用目標(biāo)事件的次數(shù)。
在leading模式下,會(huì)在指定時(shí)間間隔的開(kāi)始處觸發(fā)調(diào)用;Trailing模式下,會(huì)在指定時(shí)間間隔的末尾處觸發(fā)調(diào)用。
1) Throttle Leading

2) Throttle Trailing

3) Debounce Trailing

4) 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對(duì)象所有者即將釋放時(shí),即不再使用block時(shí)調(diào)用invalidate,該方法會(huì)將持有的task block置空,防止循環(huán)引用。如果是在頁(yè)面中使用Throttle或Debounce對(duì)象,可在disappear回調(diào)中調(diào)用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默認(rèn)模式為L(zhǎng)eading,因?yàn)閷?shí)際使用中,多數(shù)的Throttle場(chǎng)景是在指定時(shí)間間隔的開(kāi)始處調(diào)用,比如防止按鈕重復(fù)點(diǎn)擊時(shí),一般會(huì)響應(yīng)第一次點(diǎn)擊,而忽略之后的點(diǎn)擊。
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默認(rèn)模式為Trailing,因?yàn)閷?shí)際使用中,多數(shù)的Debounce場(chǎng)景是在指定時(shí)間間隔的末尾處調(diào)用,比如監(jiān)聽(tīng)用戶輸入時(shí),一般是在用戶停止輸入后再觸發(fā)調(diào)用。
核心代碼:
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;
}
總結(jié)
希望此篇文章能幫助你全面理解Throttle和Debounce的概念,趕快看看項(xiàng)目中有哪些可以用到Throttle或Debounce來(lái)提升性能的地方吧。
再次附上OC實(shí)現(xiàn)HWThrottle[3],歡迎issue和討論。
參考資料
Lodash documentation: https://lodash.com/docs/
[2]MessageThrottle: https://github.com/yulingtianxia/MessageThrottle
[3]HWThrottle: https://github.com/HighwayLaw/HWThrottle
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來(lái),可以說(shuō)是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取