在做语音播放时,发现会太频繁调用接口导致体验不太好,这时候加上函数防抖就能很好的解决这个问题。
在Rx中,有现成的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)
这两个方法内部调用方法大致相同,使用了DispatchSource的timer.schedule(deadline: deadline, leeway: self.leeway)
方法:
func scheduleRelative(_ 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
}
区别在于是传入的dueTime是给定的值还是dueTime - timeIntervalSinceLast。这也好理解:
Debouncing 函数防抖 指函数调用一次之后,距离下一次调用时间是固定的,也就是说一个函数执行过一次以后,在一段时间内不能再次执行。比如,一个函数执行完了之后,100毫秒之内不能第二次执行;
而Throttling 函数节流 指的是固定时间间隔内,执行的次数固定。
参考这种写法,我们可以使用asyncAfter实现一个Swift版本的debounce:
//不带参数版本
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) -> Void
//带参数版本
func debounce(interval: Int, queue: DispatchQueue, action: @escaping Debounce) -> Debounce {
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)
}
}
}
}
实际上除了使用时间戳判断的方式,还可以使用iOS8之后新增的DispatchWorkItem实现。
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 中,当新的任务进入时,再取消它,是的,取消。
//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()
}
}
//对应的Throttling实现
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)
}
}
}
}
另外杨萧玉有实现一个OC版本 MessageThrottle
其原理是使用Runtime Hook。
模块拆分成了3个类:
- MTRule 为消息节流的规则,内部持有target,节流消息的 SEL ,时间的阈值等属性。
- MTEngine 为调度器,功能包括获取规则,应用规则以及销毁等。
- MTInvocation则是用于消息转发。
- MTDealloc ,用于当rule的target释放时相应的SEL移除操作。
在一开始applyRule 时,就会使用objc_setAssociatedObject 将selector 作为key把MTDealloc 绑定上去以便于后续discard废弃时的相关操作;
MTEngine 中则使用弱引用的NSMapTable *targetSELs 作为存储结构,将target作为key,对应的value为其selector集合,这样我们就可以通过target 拿到selectors ,再通过每一个selector拿到MTDealloc 对象,最后拿到mtDealloc 的rule。
applyRule 方法则是首先判断对应rule是否合理,并且没有继承关系,则执行mt_overrideMethod 方法利用消息转发流程 hook 的三个步骤:
- 给类添加一个新的方法 fixed_selector,对应实现为 rule.selector 的 IMP。
- 利用 Objective-C runtime 消息转发机制,将 rule.selector 对应的 IMP 改成 _objc_msgForward 从而触发调用
forwardInvocation:
方法。 - 将
forwardInvocation:
的实现替换为自己实现的 IMP 即mt_forwardInvocation 方法,并在自己实现的逻辑中将 invocation.selector 设为 fixed_selector,并限制 [invocation invoke] 的调用频率。
最后到了mt_handleInvocation 方法,根据MTPerformMode 类型处理执行 NSInvocation ,比如在MTPerformModeDebounce 场景下,由于是异步延时执行invoke,需要调用[invocation retainArguments]; 方法保留参数,它会将传入的所有参数以及target都retain一遍,防止之后被释放,然后再检测durationThreshold 时间到来是否还是上一次的lastInvocation,因为没有变化则表示这段时间内没有新的消息,调用invoke 即可。