全局统一的定时器

要求

  • Swift 5.0+

说明

我们开发时候经常会遇到使用定时器的场景,而直接使用系统默认的Timer又比较麻烦;

虽然有其他方案可以解决使用系统Timer的各种问题,不过对于同时有多个定时器,且需要统一时间的场景,也是比较麻烦。

所以YYGlobalTimer应运而生。。。

特点

  1. 内部使用系统Timer,在一个单独的子线程上运行,没有任务时,Timer不执行
  2. 不会强引用target
  3. 当target释放时,上面的所有任务会被自动清除
  4. 闭包方式使用
  5. 添加和移除操作在子线程,加了锁
  6. 可以选择让任务运行在各种队列,默认main

添加任务Api如下:

typealias Handler = (_ currentDate: Date) -> Void

/// 添加任务,添加一个任务后timer会自动开始
///
/// - parameter target:      任务的目标对象,不指定就是YYGlobalTimer.shared,target销毁后,任务自动清除
/// - parameter key:         任务的名字
/// - parameter interval:    任务执行的间隔,单位秒,最小粒度是0.05,注意,比如0.0533会被修正为0.05
/// - parameter queue:       任务执行的队列
/// - parameter action:      具体任务
class func addTask(on target: AnyObject = YYGlobalTimer.shared,
                   forKey key: String,
                   interval: TimeInterval,
                   queue: DispatchQueue = .main,
                   action: @escaping Handler)

使用Demo

简单使用场景如下:

  • 注意task1,添加任务时没有指定target,需要手动移除

  • task2和task3在ViewController释放后会自动被移除

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        YYGlobalTimer.addTask(forKey: "task1", interval: 0.1111) { currentDate in
            print("task1  date:\(currentDate) thread\(Thread.current)")
        }

        YYGlobalTimer.addTask(on: self, forKey: "task2", interval: 0.323, queue: .global()) { currentDate in
            print("task2  date:\(currentDate) thread\(Thread.current)")
        }

        YYGlobalTimer.addTask(on: self, forKey: "task3", interval: 1.999) { currentDate in
            print("task3  date:\(currentDate) thread\(Thread.current)")
        }
    }

    deinit {
        /// 添加时没有指定target的,需要手动移除
        YYGlobalTimer.removeTasks(forKey: "task1")
    }
}

完整实现代码如下:

public final class YYGlobalTimer {
    public static let shared = YYGlobalTimer()

    public var isRunning: Bool { timer.fireDate == .distantFuture }

    /// 任务容器
    private typealias TaskDict = [String: Task]
    private var targetTasksDict = [String: TaskDict]()

    /// 线程
    private lazy var thread = Thread(target: self,
                                     selector: #selector(threadTask),
                                     object: nil)

    private lazy var semaphore = DispatchSemaphore(value: 1)

    /// 内部timer
    private lazy var timer = Timer(fireAt: .distantFuture,
                                   interval: interval,
                                   target: self,
                                   selector: #selector(timerTask),
                                   userInfo: nil,
                                   repeats: true)

    /// timer每次开始运行的总时间,毫秒,每次start都会清0
    private var duration: Millisecond = 0

    /// 内部定时器间隔时间,默认0.05秒
    private let interval = 0.05

    private init() {
        addTimerThread()
    }
}

// MARK: - Public

public extension YYGlobalTimer {
    typealias Handler = (_ currentDate: Date) -> Void

    /// 添加任务,添加一个任务后timer会自动开始
    ///
    /// - parameter target:      任务的目标对象,不指定就是YYGlobalTimer.shared,target销毁后,任务自动清除
    /// - parameter key:         任务的名字
    /// - parameter interval:    任务执行的间隔,单位秒,最小粒度是0.05,注意,比如0.0533会被修正为0.05
    /// - parameter queue:       任务执行的队列
    /// - parameter action:      具体任务
    class func addTask(on target: AnyObject = YYGlobalTimer.shared,
                       forKey key: String,
                       interval: TimeInterval,
                       queue: DispatchQueue = .main,
                       action: @escaping Handler) {
        shared.addTask(on: target,
                       forKey: key,
                       interval: interval,
                       queue: queue,
                       action: action)
    }

    /// 判断target对象上是否有key任务
    class func hasTask(on target: AnyObject? = nil,
                       forKey key: String) -> Bool {
        shared.hasTask(on: target, forKey: key)
    }

    /// 移除任务,没有任务的话timer会自动停止
    class func removeTask(on target: AnyObject? = nil,
                          forKey key: String? = nil) {
        shared.removeTask(on: target, forKey: key)
    }

    /// 暂停任务
    class func pauseTask(on target: AnyObject? = nil,
                         forKey key: String) {
        shared.pauseTask(on: target, forKey: key)
    }

    /// 恢复任务
    class func resumeTask(on target: AnyObject? = nil,
                          forKey key: String) {
        shared.resumeTask(on: target, forKey: key)
    }

    /// 移除所有任务,危险操作
    class func removeAllTasks() {
        shared.removeAllTask()
    }
}

// MARK: -  Thread & Timer

private extension YYGlobalTimer {
    func addTimerThread() {
        thread.start()
    }

    @objc func threadTask() {
        autoreleasepool {
            thread.name = "YYGlobalTimerThread"
            RunLoop.current.add(timer, forMode: .common)
            RunLoop.current.run()
        }
    }

    @objc func timerTask() {
        let currentDate = Date()
        var hasTask = false

        semaphore.wait()

        targetTasksDict.forEach { _, targetDict in
            targetDict.forEach { _, task in
                /// 目标对象释放掉,删除target上的所有任务
                if task.target == nil {
                    targetTasksDict.removeValue(forKey: task.targetName)
                } else {
                    /// 只有duration是任务执行间隔时间的倍数时,才执行该任务
                    if duration > 0,
                        !task.isPaused,
                        duration % task.interval == 0 {
                        let execute = {
                            if task.target != nil {
                                task.task(currentDate)
                            }
                        }

                        task.queue.async(execute: execute)
                    }
                    hasTask = true
                }
            }
        }

        duration += UInt(interval * 1000)

        /// 如果没有任务,暂停timer
        if !hasTask {
            pause()
        }

        semaphore.signal()
    }
}

// MARK: - Add & Remove Task

private extension YYGlobalTimer {
    typealias Millisecond = UInt

    class Task {
        weak var target: AnyObject?
        var isPaused = false
        var targetName: String

        var task: Handler
        var taskName: String

        var queue: DispatchQueue
        var interval: Millisecond // 任务的执行间隔,毫秒

        init(target: AnyObject,
             targetName: String,
             task: @escaping Handler,
             taskName: String,
             queue: DispatchQueue,
             interval: Millisecond) {
            self.target = target
            self.targetName = targetName
            self.task = task
            self.taskName = taskName
            self.queue = queue
            self.interval = interval
        }
    }

    func addTask(on target: AnyObject,
                 forKey key: String,
                 interval: TimeInterval,
                 queue: DispatchQueue = .main,
                 action: @escaping Handler) {
        let target = target
        let targetKey = _targetKey(for: target)

        /// 转换成对应的毫秒
        let intervalMS = UInt((floor(interval * 10.0) / 10) * 1000)

        let task = Task(target: target,
                        targetName: targetKey,
                        task: action,
                        taskName: key,
                        queue: queue,
                        interval: intervalMS)
        addTask(task)
    }

    func addTask(_ task: Task) {
        semaphore.wait()
        /// 值类型,注意
        if targetTasksDict[task.targetName] != nil {
            _ = self.targetTasksDict[task.targetName]?.updateValue(task, forKey: task.taskName)
        } else {
            let targetTasksDict = [task.taskName: task]
            self.targetTasksDict[task.targetName] = targetTasksDict
        }

        startIfNeeded()
        semaphore.signal()
    }

    func removeTask(on target: AnyObject? = nil, forKey key: String? = nil) {
        if target == nil, key == nil {
            return
        }

        let targetKey = _targetKey(for: target)

        semaphore.wait()
        if let taskKey = key {
            /// 删除target上指定任务
            _ = targetTasksDict[targetKey]?.removeValue(forKey: taskKey)
        } else {
            /// 删除target上的所有任务
            targetTasksDict.removeValue(forKey: targetKey)
        }
        pauseIfNeeded()

        semaphore.signal()
    }

    func removeAllTask() {
        semaphore.wait()

        targetTasksDict.removeAll()
        pause()

        semaphore.signal()
    }

    func task(on target: AnyObject?, forKey key: String) -> Task? {
        var task: Task?
        let targetKey = _targetKey(for: target)
        semaphore.wait()
        task = targetTasksDict[targetKey]?[key]
        semaphore.signal()

        return task
    }

    func hasTask(on target: AnyObject?, forKey key: String) -> Bool {
        return task(on: target, forKey: key) != nil
    }

    /// 暂停任务
    func pauseTask(on target: AnyObject?,
                   forKey key: String) {
        task(on: target, forKey: key)?.isPaused = true
    }

    /// 恢复任务
    func resumeTask(on target: AnyObject?,
                    forKey key: String) {
        task(on: target, forKey: key)?.isPaused = false
    }

    func _targetKey(for target: AnyObject?) -> String {
        return "\(target ?? self)"
    }
}

// MARK: - Start & Pause

private extension YYGlobalTimer {
    func startIfNeeded() {
        if !isRunning, targetTasksDict.values.count > 0 {
            start()
        }
    }

    func pauseIfNeeded() {
        if isRunning, targetTasksDict.values.count == 0 {
            pause()
        }
    }

    func start() {
        timer.fireDate = .init()
        duration = 0
    }

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