函數節流(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

相關文章
相關標籤/搜索