iOS中的Throttle(函數節流)與Debounce(函數防抖)


https://www.jianshu.com/p/924c3047009e

 

為什么需要Throttle和Debounce

Throttle和Debounce在前端開發可能比較經常用到,做iOS開發可能很多人不知道這個這個概念,其實很開發者在工作中或多或少都遇到過,就像設計模式有很多種,開發中用到了某種設計模式自己卻不知道,這篇文章我們就簡單聊Throttle和Debounce。
開發中我們都遇到頻率很高的事件(如搜索框的搜索)或者連續事件(如UIScrollView的contentOffset進行某些計算),這個時候為了進行性能優化就要用到Throttle和Debounce。在詳細說這連個概念之前我們先弄清楚一件事就是觸發事件和執行事件對應的方法是不同的。舉個栗子,有個button,我們點擊是觸發了點擊事件和之后比如進行網絡這個方法是不一樣的,Throttle和Debounce並不會限制你去觸發點擊事件,但是會控制之后的方法調用,這和我們設置一種機制,去設置button的isEnable的方式是不同的。

Debounce

當事件觸發超過一段時間之后才會執行方法,如果在這段時間之內有又觸發了這個時間,則重新計算時間。
電梯的處理就和這個類似,比如現在在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實現的,讀者感興趣可以自己看看

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

預先設定一個執行周期,當調用動作大於等於執行周期則執行該動作,然后進入下一個新的時間周期
這有點像班車系統和這個類似,比如一個班車每隔15分鍾發車,有人來了就上車,到了15分鍾就發車,不管中間有多少乘客上車。

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 ?? "") } } } } 

思考

根據Debounce我們知道如果一直去觸發某個事件,那么就會造成一直無法調用相應的方法,那么我們可以設置一個最大等待時間maxInterval,當超過這個時間則執行相應的方法,避免一直等待。具體實施就不寫了,讀者結合Debounce和Throttle可以自己去實現,哈哈,這個有點像Debounce和Throttle的雜交品種。

參考文章



作者:小涼介
鏈接:https://www.jianshu.com/p/924c3047009e
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM