阐述
前面已经介绍 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