在做語音播放時,發(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 的三個步驟:
- 給類添加一個新的方法 fixed_selector,對應(yīng)實(shí)現(xiàn)為 rule.selector 的 IMP搏明。
- 利用 Objective-C runtime 消息轉(zhuǎn)發(fā)機(jī)制鼠锈,將 rule.selector 對應(yīng)的 IMP 改成 _objc_msgForward 從而觸發(fā)調(diào)用
forwardInvocation:
方法。 - 將
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 即可。