Swift Debouncing & Throttling

在做语音播放时,发现会太频繁调用接口导致体验不太好,这时候加上函数防抖就能很好的解决这个问题。

在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 的三个步骤:

  1. 给类添加一个新的方法 fixed_selector,对应实现为 rule.selector 的 IMP。
  2. 利用 Objective-C runtime 消息转发机制,将 rule.selector 对应的 IMP 改成 _objc_msgForward 从而触发调用 forwardInvocation: 方法。
  3. forwardInvocation: 的实现替换为自己实现的 IMP 即mt_forwardInvocation 方法,并在自己实现的逻辑中将 invocation.selector 设为 fixed_selector,并限制 [invocation invoke] 的调用频率。

最后到了mt_handleInvocation 方法,根据MTPerformMode 类型处理执行 NSInvocation ,比如在MTPerformModeDebounce 场景下,由于是异步延时执行invoke,需要调用[invocation retainArguments]; 方法保留参数,它会将传入的所有参数以及target都retain一遍,防止之后被释放,然后再检测durationThreshold 时间到来是否还是上一次的lastInvocation,因为没有变化则表示这段时间内没有新的消息,调用invoke 即可。

你可能感兴趣的:(Swift Debouncing & Throttling)