Swift Debouncing & Throttling

在做語音播放時,發(fā)現(xiàn)會太頻繁調(diào)用接口導(dǎo)致體驗(yàn)不太好缕棵,這時候加上函數(shù)防抖就能很好的解決這個問題瘩绒。

在Rx中困肩,有現(xiàn)成的debounce和throttle方法:

       let disposeBag = DisposeBag()
        Observable.of(1,2,3)
            .debounce(1, scheduler: MainScheduler.instance)
            .subscribe(onNext: {print($0)})
            .disposed(by: disposeBag)
        
        exitBtn.rx.tap
            .throttle(1, scheduler: MainScheduler.instance)
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)

這兩個方法內(nèi)部調(diào)用方法大致相同,使用了DispatchSource的timer.schedule(deadline: deadline, leeway: self.leeway)方法:

    func scheduleRelative<StateType>(_ state: StateType, dueTime: Foundation.TimeInterval, action: @escaping (StateType) -> Disposable) -> Disposable {
        let deadline = DispatchTime.now() + dispatchInterval(dueTime)
        let compositeDisposable = CompositeDisposable()
        let timer = DispatchSource.makeTimerSource(queue: self.queue)
        timer.schedule(deadline: deadline, leeway: self.leeway)
        var timerReference: DispatchSourceTimer? = timer
        let cancelTimer = Disposables.create {
            timerReference?.cancel()
            timerReference = nil
        }
        timer.setEventHandler(handler: {
            if compositeDisposable.isDisposed {
                return
            }
            _ = compositeDisposable.insert(action(state))
            cancelTimer.dispose()
        })
        timer.resume()
        _ = compositeDisposable.insert(cancelTimer)
        return compositeDisposable
    }

區(qū)別在于是傳入的dueTime是給定的值還是dueTime - timeIntervalSinceLast拗窃。這也好理解:

Debouncing 函數(shù)防抖 指函數(shù)調(diào)用一次之后昔园,距離下一次調(diào)用時間是固定的,也就是說一個函數(shù)執(zhí)行過一次以后并炮,在一段時間內(nèi)不能再次執(zhí)行默刚。比如,一個函數(shù)執(zhí)行完了之后逃魄,100毫秒之內(nèi)不能第二次執(zhí)行荤西;

Throttling 函數(shù)節(jié)流 指的是固定時間間隔內(nèi),執(zhí)行的次數(shù)固定。

參考這種寫法邪锌,我們可以使用asyncAfter實(shí)現(xiàn)一個Swift版本的debounce:

        //不帶參數(shù)版本
    func debounce(interval: Int, queue: DispatchQueue, action: @escaping (() -> Void)) -> () -> Void {
        var lastFire = DispatchTime.now()
        let delay = DispatchTimeInterval.milliseconds(interval)
        
        return {
            lastFire = DispatchTime.now()
            let dispatchTime = DispatchTime.now() + delay

            queue.asyncAfter(deadline: dispatchTime) {
                let when = lastFire + delay
                let now = DispatchTime.now()
                if now.rawValue >= when.rawValue {
                    action()
                }
            }
        }
    }
    typealias Debounce<T> = (_ : T) -> Void
        //帶參數(shù)版本    
    func debounce<T>(interval: Int, queue: DispatchQueue, action: @escaping Debounce<T>) -> Debounce<T> {
        var lastFire = DispatchTime.now()
        let delay = DispatchTimeInterval.milliseconds(interval)
        
        return { param in
            lastFire = DispatchTime.now()
            let dispatchTime = DispatchTime.now() + delay
            
            queue.asyncAfter(deadline: dispatchTime) {
                let when = lastFire + delay
                let now = DispatchTime.now()
                if now.rawValue >= when.rawValue {
                    action(param)
                }
            }
        }
    }

實(shí)際上除了使用時間戳判斷的方式勉躺,還可以使用iOS8之后新增的DispatchWorkItem實(shí)現(xiàn)。

DispatchWorkItem encapsulates work that can be performed. A work item can be dispatched onto a DispatchQueue and within a DispatchGroup. A DispatchWorkItem can also be set as a DispatchSource event, registration, or cancel handler.

我們可以將代碼封裝在 DispatchWorkItem 中觅丰,當(dāng)新的任務(wù)進(jìn)入時饵溅,再取消它,是的妇萄,取消蜕企。

//stackoverflow版本
class Debouncer {
    private let queue = DispatchQueue.main
    private var workItem = DispatchWorkItem(block: {})
    private var interval: TimeInterval

    init(seconds: TimeInterval) {
        self.interval = seconds
    }

    func debounce(action: @escaping (() -> Void)) {
        workItem.cancel()
        workItem = DispatchWorkItem(block: { action() })
        queue.asyncAfter(deadline: .now() + interval, execute: workItem)
    }
}
//改造為線程安全版本的 Debouncing
class Debouncer {
    private let queue: DispatchQueue
    private let interval: TimeInterval
    private let semaphore: DebouncerSemaphore
    private var workItem: DispatchWorkItem?

    init(seconds: TimeInterval, qos: DispatchQoS = .default) {
        interval = seconds
        semaphore = DebouncerSemaphore(value: 1)
        queue = DispatchQueue(label: "debouncer.queue", qos: qos)
    }
    
    func invoke(_ action: @escaping (() -> Void)) {
        semaphore.sync {
            workItem?.cancel()
            workItem = DispatchWorkItem(block: {
                action()
            })
            if let item = workItem {
                queue.asyncAfter(deadline: .now() + self.interval, execute: item)
            }
        }
    }
}

struct DebouncerSemaphore {
    private let semaphore: DispatchSemaphore
    
    init(value: Int) {
        semaphore = DispatchSemaphore(value: value)
    }
    
    func sync(execute: () -> Void) {
        defer { semaphore.signal() }
        semaphore.wait()
        execute()
    }
}
//對應(yīng)的Throttling實(shí)現(xiàn)
class Throttler {
    private let queue: DispatchQueue
    private let interval: TimeInterval
    private let semaphore: DebouncerSemaphore
    private var workItem: DispatchWorkItem?
    private var lastExecuteTime = Date()
    
    init(seconds: TimeInterval, qos: DispatchQoS = .default) {
        interval = seconds
        semaphore = DebouncerSemaphore(value: 1)
        queue = DispatchQueue(label: "throttler.queue", qos: qos)
    }
    
    func invoke(_ action: @escaping (() -> Void)) {
        semaphore.sync {
            workItem?.cancel()
            workItem = DispatchWorkItem(block: { [weak self] in
                self?.lastExecuteTime = Date()
                action()
            })
            let deadline = Date().timeIntervalSince(lastExecuteTime) > interval ? 0 : interval
            if let item = workItem {
                queue.asyncAfter(deadline: .now() + deadline, execute: item)
            }
        }
    }
}

另外楊蕭玉有實(shí)現(xiàn)一個OC版本 MessageThrottle

其原理是使用Runtime Hook。
模塊拆分成了3個類:

  • MTRule 為消息節(jié)流的規(guī)則冠句,內(nèi)部持有target轻掩,節(jié)流消息的 SEL ,時間的閾值等屬性懦底。
  • MTEngine 為調(diào)度器唇牧,功能包括獲取規(guī)則,應(yīng)用規(guī)則以及銷毀等聚唐。
  • MTInvocation則是用于消息轉(zhuǎn)發(fā)丐重。
  • MTDealloc ,用于當(dāng)rule的target釋放時相應(yīng)的SEL移除操作杆查。

在一開始applyRule 時扮惦,就會使用objc_setAssociatedObject 將selector 作為key把MTDealloc 綁定上去以便于后續(xù)discard廢棄時的相關(guān)操作;

MTEngine 中則使用弱引用的NSMapTable *targetSELs 作為存儲結(jié)構(gòu)根灯,將target作為key径缅,對應(yīng)的value為其selector集合,這樣我們就可以通過target 拿到selectors 烙肺,再通過每一個selector拿到MTDealloc 對象纳猪,最后拿到mtDealloc 的rule。

applyRule 方法則是首先判斷對應(yīng)rule是否合理桃笙,并且沒有繼承關(guān)系氏堤,則執(zhí)行mt_overrideMethod 方法利用消息轉(zhuǎn)發(fā)流程 hook 的三個步驟:

  1. 給類添加一個新的方法 fixed_selector,對應(yīng)實(shí)現(xiàn)為 rule.selector 的 IMP搏明。
  2. 利用 Objective-C runtime 消息轉(zhuǎn)發(fā)機(jī)制鼠锈,將 rule.selector 對應(yīng)的 IMP 改成 _objc_msgForward 從而觸發(fā)調(diào)用 forwardInvocation: 方法。
  3. forwardInvocation: 的實(shí)現(xiàn)替換為自己實(shí)現(xiàn)的 IMP 即mt_forwardInvocation 方法星著,并在自己實(shí)現(xiàn)的邏輯中將 invocation.selector 設(shè)為 fixed_selector购笆,并限制 [invocation invoke] 的調(diào)用頻率。

最后到了mt_handleInvocation 方法虚循,根據(jù)MTPerformMode 類型處理執(zhí)行 NSInvocation 同欠,比如在MTPerformModeDebounce 場景下样傍,由于是異步延時執(zhí)行invoke,需要調(diào)用[invocation retainArguments]; 方法保留參數(shù)铺遂,它會將傳入的所有參數(shù)以及target都retain一遍衫哥,防止之后被釋放,然后再檢測durationThreshold 時間到來是否還是上一次的lastInvocation襟锐,因?yàn)闆]有變化則表示這段時間內(nèi)沒有新的消息撤逢,調(diào)用invoke 即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末粮坞,一起剝皮案震驚了整個濱河市蚊荣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌捞蚂,老刑警劉巖妇押,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跷究,死亡現(xiàn)場離奇詭異姓迅,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)俊马,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門丁存,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柴我,你說我怎么就攤上這事解寝。” “怎么了艘儒?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵聋伦,是天一觀的道長。 經(jīng)常有香客問我界睁,道長觉增,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任翻斟,我火速辦了婚禮逾礁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘访惜。我一直安慰自己嘹履,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布债热。 她就那樣靜靜地躺著砾嫉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窒篱。 梳的紋絲不亂的頭發(fā)上焕刮,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天蚓峦,我揣著相機(jī)與錄音,去河邊找鬼济锄。 笑死暑椰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荐绝。 我是一名探鬼主播一汽,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼低滩!你這毒婦竟也來了召夹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤恕沫,失蹤者是張志新(化名)和其女友劉穎监憎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體婶溯,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鲸阔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了迄委。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褐筛。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖叙身,靈堂內(nèi)的尸體忽然破棺而出渔扎,到底是詐尸還是另有隱情,我是刑警寧澤信轿,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布晃痴,位于F島的核電站,受9級特大地震影響财忽,放射性物質(zhì)發(fā)生泄漏倘核。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一定罢、第九天 我趴在偏房一處隱蔽的房頂上張望笤虫。 院中可真熱鬧,春花似錦祖凫、人聲如沸琼蚯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遭庶。三九已至,卻和暖如春稠屠,著一層夾襖步出監(jiān)牢的瞬間峦睡,已是汗流浹背翎苫。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榨了,地道東北人煎谍。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像龙屉,于是被迫代替她去往敵國和親呐粘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

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