iOS中的Throttle(函數(shù)節(jié)流)與Debounce(函數(shù)防抖)

為什么需要Throttle和Debounce

Throttle和Debounce在前端開發(fā)可能比較經(jīng)常用到周瞎,做iOS開發(fā)可能很多人不知道這個這個概念相赁,其實很開發(fā)者在工作中或多或少都遇到過,就像設計模式有很多種呵曹,開發(fā)中用到了某種設計模式自己卻不知道款咖,這篇文章我們就簡單聊Throttle和Debounce。
開發(fā)中我們都遇到頻率很高的事件(如搜索框的搜索)或者連續(xù)事件(如UIScrollView的contentOffset進行某些計算)奄喂,這個時候為了進行性能優(yōu)化就要用到Throttle和Debounce铐殃。在詳細說這連個概念之前我們先弄清楚一件事就是觸發(fā)事件和執(zhí)行事件對應的方法是不同的。舉個栗子跨新,有個button富腊,我們點擊是觸發(fā)了點擊事件和之后比如進行網(wǎng)絡這個方法是不一樣的,Throttle和Debounce并不會限制你去觸發(fā)點擊事件玻蝌,但是會控制之后的方法調(diào)用蟹肘,這和我們設置一種機制词疼,去設置button的isEnable的方式是不同的俯树。

Debounce

當事件觸發(fā)超過一段時間之后才會執(zhí)行方法帘腹,如果在這段時間之內(nèi)有又觸發(fā)了這個時間,則重新計算時間许饿。
電梯的處理就和這個類似阳欲,比如現(xiàn)在在4樓,有個人按了1樓的按鈕(事件)陋率,這個時候電梯會等一固定時間球化,如果沒人再按按鈕,則電梯開始下降(對應的方法)瓦糟,如果有人立馬又按了1樓按鈕筒愚,電梯就會重新計算時間。
我們看看在面對search問題上可以怎么處理

第一版

class SearchViewController: UIViewController, UISearchBarDelegate {
    // We keep track of the pending work item as a property
    private var pendingRequestWorkItem: DispatchWorkItem?

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        // Cancel the currently pending item
        pendingRequestWorkItem?.cancel()

        // Wrap our request in a work item
        let requestWorkItem = DispatchWorkItem { [weak self] in
            self?.resultsLoader.loadResults(forQuery: searchText)
        }

        // Save the new work item and execute it after 250 ms
        pendingRequestWorkItem = requestWorkItem
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
                                      execute: requestWorkItem)
    }
}

這里運用了DispatchWorkItem菩浙,將請求放在代碼塊中巢掺,當有一個請求來時我們可以輕易的取消請求。正如你上面看到的劲蜻,使用DispatchWorkItem在Swift中實際上比使用Timer或者Operation要好得多陆淀,這要歸功于尾隨的閉包語法,以及GCD如何導入Swift先嬉。 你不需要@objc標記的方法轧苫,或#selector,它可以全部使用閉包完成疫蔓。

第二版
但只是這樣肯定不行的含懊,我們試著去封裝一下好在其他地方也能同樣使用。下面我們看看參考文章里的一個寫法衅胀,當然還有用Timer實現(xiàn)的绢要,讀者感興趣可以自己看看

typealias Debounce<T> = (_ : T) -> Void

func debounce<T>(interval: Int, queue: DispatchQueue, action: @escaping Debounce<T>) -> Debounce<T> {
    var lastFireTime = DispatchTime.now()
    let dispatchDelay = DispatchTimeInterval.milliseconds(interval)

    return { param in
        lastFireTime = DispatchTime.now()
        let dispatchTime: DispatchTime = DispatchTime.now() + dispatchDelay

        queue.asyncAfter(deadline: dispatchTime) {
            let when: DispatchTime = lastFireTime + dispatchDelay
            let now = DispatchTime.now()

            if now.rawValue >= when.rawValue {
                action(param)
            }
        }
    }
}

第三版

下面我們再對其進行改進,一是使用DispatchWorkItem拗小,二是使用DispatchSemaphore保證線程安全重罪。

class Debouncer {
    public let label: String
    public let interval: DispatchTimeInterval
    fileprivate let queue: DispatchQueue
    fileprivate let semaphore: DispatchSemaphoreWrapper
    fileprivate var workItem: DispatchWorkItem?
    
    
    public init(label: String, interval: Float, qos: DispatchQoS = .userInteractive) {
        self.interval         = .milliseconds(Int(interval * 1000))
        self.label         = label
        self.queue = DispatchQueue(label: "com.farfetch.debouncer.internalqueue.\(label)", qos: qos)
        self.semaphore = DispatchSemaphoreWrapper(withValue: 1)
    }
    
    
    public func call(_ callback: @escaping (() -> ())) {
        
        self.semaphore.sync  { () -> () in
            
            
            self.workItem?.cancel()
            
            self.workItem = DispatchWorkItem {
                callback()
            }
            
            if let workItem = self.workItem {
                
                self.queue.asyncAfter(deadline: .now() + self.interval, execute: workItem)
            }
        }
    }
    
}


public struct DispatchSemaphoreWrapper {
    
    private let semaphore: DispatchSemaphore
    
    public init(withValue value: Int) {
        
        self.semaphore = DispatchSemaphore(value: value)
    }
    
    public func sync<R>(execute: () throws -> R) rethrows -> R {
        
        _ = semaphore.wait(timeout: DispatchTime.distantFuture)
        defer { semaphore.signal() }
        return try execute()
    }
}

Throttle

預先設定一個執(zhí)行周期,當調(diào)用動作大于等于執(zhí)行周期則執(zhí)行該動作哀九,然后進入下一個新的時間周期
這有點像班車系統(tǒng)和這個類似剿配,比如一個班車每隔15分鐘發(fā)車,有人來了就上車阅束,到了15分鐘就發(fā)車呼胚,不管中間有多少乘客上車。

import UIKit
import Foundation
 
public class Throttler {
    
    private let queue: DispatchQueue = DispatchQueue.global(qos: .background)
    
    private var job: DispatchWorkItem = DispatchWorkItem(block: {})
    private var previousRun: Date = Date.distantPast
    private var maxInterval: Int
    fileprivate let semaphore: DispatchSemaphoreWrapper
    
    init(seconds: Int) {
        self.maxInterval = seconds
        self.semaphore = DispatchSemaphoreWrapper(withValue: 1)
    }
    
    
    func throttle(block: @escaping () -> ()) {
        
        self.semaphore.sync  { () -> () in
            job.cancel()
            job = DispatchWorkItem(){ [weak self] in
                self?.previousRun = Date()
                block()
            }
            let delay = Date.second(from: previousRun) > maxInterval ? 0 : maxInterval
            queue.asyncAfter(deadline: .now() + Double(delay), execute: job)
        }
        
    }
}
 
private extension Date {
    static func second(from referenceDate: Date) -> Int {
        return Int(Date().timeIntervalSince(referenceDate).rounded())
    }
}

示例

import UIKit
 
public class SearchBar: UISearchBar, UISearchBarDelegate {
    
    /// Throttle engine
    private var throttler: Throttler? = nil
    
    /// Throttling interval
    public var throttlingInterval: Double? = 0 {
        didSet {
            guard let interval = throttlingInterval else {
                self.throttler = nil
                return
            }
            self.throttler = Throttler(seconds: interval)
        }
    }
    
    /// Event received when cancel is pressed
    public var onCancel: (() -> (Void))? = nil
    
    /// Event received when a change into the search box is occurred
    public var onSearch: ((String) -> (Void))? = nil
    
    public override func awakeFromNib() {
        super.awakeFromNib()
        self.delegate = self
    }
    
    // Events for UISearchBarDelegate
    
    public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        self.onCancel?()
    }
    
    public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        self.onSearch?(self.text ?? "")
    }
    
    public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        guard let throttler = self.throttler else {
            self.onSearch?(searchText)
            return
        }
        throttler.throttle {
            DispatchQueue.main.async {
                self.onSearch?(self.text ?? "")
            }
        }
    }
    
}

思考

根據(jù)Debounce我們知道如果一直去觸發(fā)某個事件息裸,那么就會造成一直無法調(diào)用相應的方法蝇更,那么我們可以設置一個最大等待時間maxInterval沪编,當超過這個時間則執(zhí)行相應的方法,避免一直等待年扩。具體實施就不寫了蚁廓,讀者結合Debounce和Throttle可以自己去實現(xiàn),哈哈厨幻,這個有點像Debounce和Throttle的雜交品種相嵌。

參考文章

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市况脆,隨后出現(xiàn)的幾起案子饭宾,更是在濱河造成了極大的恐慌,老刑警劉巖格了,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件看铆,死亡現(xiàn)場離奇詭異,居然都是意外死亡盛末,警方通過查閱死者的電腦和手機弹惦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來满败,“玉大人肤频,你說我怎么就攤上這事∷隳” “怎么了宵荒?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長净嘀。 經(jīng)常有香客問我报咳,道長,這世上最難降的妖魔是什么挖藏? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任暑刃,我火速辦了婚禮,結果婚禮上膜眠,老公的妹妹穿的比我還像新娘岩臣。我一直安慰自己,他們只是感情好宵膨,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布架谎。 她就那樣靜靜地躺著,像睡著了一般辟躏。 火紅的嫁衣襯著肌膚如雪谷扣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天捎琐,我揣著相機與錄音会涎,去河邊找鬼裹匙。 笑死,一個胖子當著我的面吹牛末秃,可吹牛的內(nèi)容都是我干的概页。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蛔溃,長吁一口氣:“原來是場噩夢啊……” “哼绰沥!你這毒婦竟也來了篱蝇?” 一聲冷哼從身側響起贺待,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎零截,沒想到半個月后麸塞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡涧衙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年哪工,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弧哎。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡雁比,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出撤嫩,到底是詐尸還是另有隱情偎捎,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布棋嘲,位于F島的核電站搀擂,受9級特大地震影響档叔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丈牢,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瞄沙。 院中可真熱鬧己沛,春花似錦、人聲如沸距境。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肮疗。三九已至晶姊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間伪货,已是汗流浹背们衙。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工钾怔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蒙挑。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓宗侦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親忆蚀。 傳聞我的和親對象是個殘疾皇子矾利,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,979評論 3 119
  • 福利院有顆大榕樹馋袜,樹下有把桃紅色的塑料椅男旗,一位老奶奶坐在椅子上,雙手相扣欣鳖,半張著嘴察皇,瞇眼睡去。風泽台,掠過她桃紅色的發(fā)...
    西山有井閱讀 202評論 0 1
  • 三分酒意釀成涼什荣,且無妨,醉思量怀酷。莫道向誰稻爬,枉斷相思腸。即使天涯隨夢海蜕依,如雁字桅锄,和鴻翔。 江山無限最成傷笔横,念西窗竞滓,月...
    迷曳閱讀 755評論 5 3
  • 文/雨隨塵清 走過曲折的山路,踏過清涼的小溪吹缔,領略過晨起初生的朝陽商佑,欣賞過朦朧的月色和漫天的星光。 車窗外的風景厢塘,...
    清陋閱讀 290評論 14 17