iOS 探讨之 dispatch_source 定时器

阐述

前面已经介绍 CADisplayLink、mach_absolute_time 都可以在定时这块进行封装,当然NSTimer也是可以的,这次我就梳理一下 dispatch_source 版本的定时。

NSTimer 受 RunLoop 的影响, 由于 RunLoop 需要处理很多任务,所以其精度不高。 CADisplayLink精度低的原因类似,具体原因放在最后解释。

如果我们对定时器的精度要求很高,可以考虑使用 dispatch_source 去实现。

它精度很高,系统会自动触发,系统级别的源,并且不受RunLoopMode的影响。

探讨

Dispatch Source 是BSD系统内核惯有功能kqueue的包装,kqueue是在XNU内核中发生事件时在应用程序编程方执行处理的技术。它的CPU负荷非常小,尽量不占用资源。当事件发生时, Dispatch Source 会在指定的Dispatch Queue中执行事件的处理。

Dispatch Source 种类:

1   DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加

2   DISPATCH_SOURCE_TYPE_DATA_OR 变量 OR

3   DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送

4   DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收

5   DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 内存压力 (注:iOS8后可用)

6   DISPATCH_SOURCE_TYPE_PROC 检测到与进程相关的事件

7   DISPATCH_SOURCE_TYPE_READ 可读取文件映像

8   DISPATCH_SOURCE_TYPE_SIGNAL 接收信号

9   DISPATCH_SOURCE_TYPE_TIMER 定时器

10 DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更

11 DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像

定时器则是 DISPATCH_SOURCE_TYPE_TIMER 所对应技术处理。

代码

    // OC版本
    // 任务执行所指定的队列    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    // 当前定时器源        
    self.timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 任务执行开始时间
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC);
    
    // 任务执行间隔时间
    uint64_t interval = 1.0 * NSEC_PER_SEC;

    // 给定时器源绑定开始时间、间隔时间以及容忍误差时间
    dispatch_source_set_timer(self.timerSource, start, interval, 0);
    
    // 给定时器源绑定任务
    dispatch_source_set_event_handler(self.timerSource, ^{
        // 内部最好使用weak/strong修饰的self, 防止循环引用
        NSLog(@"----self.timer---");
    });
    
    // 启动定时器源
    dispatch_resume(self.timerSource);
    // Swift版本
    // 任务执行所指定的队列    
    fileprivate let timer: DispatchSourceTimer = DispatchSource.makeTimerSource()

    // 时钟改变间隔(默认 1s)   
    fileprivate var clockInterval: TimeInterval = 1

    fileprivate init() {
        // 给定时器源绑定开始时间、间隔时间
        self.timer.schedule(deadline: .now()+self.clockInterval, repeating: self.clockInterval)
        self.timer.setEventHandler(qos: .utility, flags: DispatchWorkItemFlags.assignCurrentContext) { [weak self] in
            guard let wself = self else { return }
            // 所执行的任务

        }
        self.timer.resume()
    }

OC 版本 dispatch_source定时器进入后台的时候会自动暂停(网上所说的开始时间用 dispatch_walltime(NULL, 0) 也不好使)。

Swift 版本进入后台不会自动暂停。

注意项

1.  source 需要手动启动

Dispatch Source 使用最多的就是用来实现定时器,source创建后默认是暂停状态,需要手动调用 dispatch_resume启动定会器。 dispatch_after只是封装调用了dispatch source定时器,然后在回调函数中执行定义的block.

2.  循环引用

因为 dispatch_source_set_event_handle回调是block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。所以正确的方法是使用weak+strong或者提前调dispatch_source_cancel取消timer.

3.  resume、suspend 调用次数保持平衡

dispatch_resume 和 dispatch_suspend 调用次数需要平衡,如果重复调用 dispatch_resume则会崩溃,因为重复调用会让dispatch_resume 代码里if分支不成立,从而执行了 DISPATCH_CLIENT_CRASH(“Over-resume of an object”) 导致崩溃。

4.  source 创建与释放时机

source在suspend状态下,如果直接设置 source = nil 或者重新创建 source 都会造成 crash。正确的方式是在resume状态下调用 dispatch_source_cancel(source)后再重新创建。

拓展

其它定时器不准的原因

1.  NSTimer

NSTimer 、 CFRunLoopTimerRef 之间是 toll-free bridged 的。

NSTimer 注册到 RunLoop 后, RunLoop 会为其重复的时间点注册事件。 如: 10:00、10:10、10:20 这几个时间点。

RunLoop 为了节省资源,并不会在非常准确的时间点回调这个 NSTimer 事件。

NSTimer 有个属性叫做 Tolerance (宽容度),标识了当前时间点所容许的最大误差。

如果这个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。

2.  CADisplayLink 

CADisplayLink 是一个和屏幕刷新率一致的定时器 , 如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,该有的事件回调则不会去触发,会造成界面卡顿,时间也不太准。

参考资料

1.  https://blog.csdn.net/wang_Bo_JustOne/article/details/76147050

2.  https://xiaozhuanlan.com/topic/9481560732

3.  https://www.jianshu.com/p/faa6ffe4fac3

4.  https://www.jianshu.com/p/edbe946c8a11?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

5.  https://www.jianshu.com/p/137885b8c140?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

猜你喜欢

转载自blog.csdn.net/yanglei3kyou/article/details/88829148