21

设计一个简单准确的定时器

 4 years ago
source link: http://yeziahehe.com/2020/04/26/design_a_simple_timer/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

项目中有定时刷新的功能需求,使用 NSTimer 一把梭之后发现了如下的问题,我们首先写一段示例代码:

nEbMFbj.png!web

1.循环引用

timer 会强引用 target,而 target 又强引用者 timer 对象,最后就会导致当前 viewcontroller 不会被释放,deinit 方法也不会被调用,从而产生了循环引用。

第一种可以通过 Timer 的另一个方法,不去使用 target,在 block 中弱引用 self,可以解决循环引用的问题,代码如下。

QnENnuM.png!web

第二种方式可以通过引入一个中间对象,让 target 强引用中间对象,中间对象在弱引用 timer,这样一旦 viewcontroller 被释放掉,deinit 方法就会被调用,timer 就会被释放掉。但是这样存在一个问题是,中间对象其实是无法响应 selector,会导致崩溃。这样就是需要用到消息转发机制,将所有中间对象收到的事件都转发给 self.target 进行响应,代码如下。

主类的代码:

Z7baaiJ.png!web

中间对象 MyProxy 的代码:

nqquErZ.png!web

其他还有些方式,可以看看这篇博客 iOS之NSTimer循环引用的解决方案 - 掘金 ,这边不做详细介绍了。

2.Runloop 导致计时不准确

由于 NSTimer 是依赖于 RunLoop 机制的,所以会因为 Runloop 的问题导致计时不准确。上面两个 Timer.scheduledTimer 初始化方法都是默认运行在 Runloop 的 default mode 中。

在 ScrollView 在滑动的过程中,主线程的 Runloop 会切换到 UITrackingRunLoopMode ,这个时候 timer 就不会运行,就会导致计时不准确。如果想要滑动的时候不失效,可以将 timer 运行在 NSRunLoopCommonModes

3.线程问题

同样由于 NSTimer 是依赖于 RunLoop 机制的,所以在子线程中初始化一个 timer 默认是不会运行的。原因是因为子线程并没有创建 Runloop。

CADisplayLink

官方文档对于 CADisplayLink 的介绍是:

A timer object that allows your application to synchronize its drawing to the refresh rate of the display.

一般情况下,我们的屏幕刷新率是1/60s 一次。CADisplayLink 跟 NSTimer 的用法基本相似,NSTimer 的时间间隔是以秒为单位,而 CADisplayLink 是使用帧率来作为时间间隔的单位。

所以说 CADisplayLink 最常见到的应用场景就是写一个监测 FPS 的工具,我这里引用 YYFPSLabel 的代码实现大概介绍下原理:

Fz6Rfib.png!web

我们可以看到,使用方法与 NSTimer 基本一致,同样是使用了 YYWeakProxy 来避免循环引用,且 CADisplayLink 还需要手动添加到 Runloop 中。那么问题在哪里?问题就出在 CADisplayLink 仍然是基于 Runloop 来实现的,而 RunLoop 的运行取决于其所在的 mode 以及 CPU 的繁忙程度,当 CPU 忙于计算显示内容或者 GPU 工作太繁重时,就会导致显示出来的 FPS 与 Instrument 的不一致。所以说基于CADisplayLink实现的 FPS 无法完全检测出当前 Core Animation 的性能情况,它只能检测出当前 RunLoop 的帧率。

GCD

最后我们说到今天的重点,通过 GCD 来实现 Timer,主要是使用 DispatchSource 。步骤如下:创建一个监听的事件类型对应的 dispatch source,然后给这个 source 指定闭包和 Dispatch Queue。当 source 监听到相应的事件时,就会将该闭包自动加到 queue 中执行。代码如下,引用自 GitHub - 100mango/SwiftTimer: Simple and Elegant Timer

u6fUbu7.png!web

我们再看看代码,循环引用、计时不准确、线程问题都得到了解决,相比较除了代码量稍微多一些,在精度方面无疑是最好的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK