iOS中的Throttle(函数节流)与Debounce(函数防抖)

为什么需要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的杂交品种。

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,967评论 3 119
  • 福利院有颗大榕树,树下有把桃红色的塑料椅,一位老奶奶坐在椅子上,双手相扣,半张着嘴,眯眼睡去。风,掠过她桃红色的发...
    西山有井阅读 201评论 0 1
  • 三分酒意酿成凉,且无妨,醉思量。莫道向谁,枉断相思肠。即使天涯随梦海,如雁字,和鸿翔。 江山无限最成伤,念西窗,月...
    迷曳阅读 745评论 5 3
  • 文/雨随尘清 走过曲折的山路,踏过清凉的小溪,领略过晨起初生的朝阳,欣赏过朦胧的月色和漫天的星光。 车窗外的风景,...
    清陋阅读 286评论 14 17