NSTimer的秘密世界

›也可以 在此链接的我的博客上找到本文

计时器允许我们在一个或多个计时器间隔后执行一些代码。
有多种类型的时钟可用于创建计时器,即使所有这些时钟显然都以相同的速率运行,它们的行为仍然不同。
我们可以列出以下计时器类型列表:

  • 实时时钟或RTC 。 它是一个计算机时钟(通常为集成电路形式),用于跟踪当前时间。 用户可以任意更改此时钟,而NTP(网络时间协议)则最好使其与外部参考保持同步。 它的值每秒增加一秒钟,但有时可能会更快/更慢或向前跳跃( 这要感谢Gavin Eadie,这使我意识到时钟永远不会向后运行.NTP同步发现时钟在运行如果运行速度快,则在尝试与外部源同步时应放慢速度,直到“实时”赶上
  • 单调计时器 。 它的计数器通过使用计时器中断发送到CPU的物理信号递增。 在Apple平台上,此值由Mach内核通过mach_absolute_time()返回。 返回值取决于CPU,因此您不能只将其乘以一个常数就可以得出真实值。 相反,您应该调用系统提供的转换函数将其转换为真实值(CoreAnimation有一个方便的方法:CACurrentMediaTime())。 它在启动时被重置的事实使得获取现实世界中流逝的时间并不那么有趣,但是它是测量两个时间间隔之间差异的最精确方法。
  • 启动计时器 。 它只是一个特殊的单调计时器 ,在系统进入睡眠状态时不会暂停。 获取其值的最常见方法是从终端调用uptime函数。

在Apple平台上,创建计时器的最常见方法是使用NSTimer类。 实际上,它只是围绕单调计时器的包装。
因此,使用NSTimer可能会以无法预料的方式结束,特别是在iOS上,在如上所述的某些极端情况下,机会性的资源使用可能会结束。

为了充分理解NSTimer,我们需要谈一些NSRunLoop; 一旦启动,每个应用程序都会创建第一个NSThread,称为Main Thread; 每个线程都有一个关联的运行循环,该循环管理输入源,例如鼠标,键盘,触摸,连接……以及显然是我们的计时器。

您可以将RunLoop视为等待新消息并将其传递给适当的收件人的邮箱 :它基本上是一种消息传递机制,用于异步或线程间通信。
一些平台(如Windows)将其称为Message Pump,但内部概念仍然相同。

实际上, 运行循环代表了命令行 应用程序 与交互式 (通常是基于UI) 应用程序 之间的主要区别
当第一个使用参数启动时,执行它们的内容,然后退出,一个交互式应用程序等待用户输入,对此做出反应并再次等待。

每个线程只有一个Run循环; 一个运行循环由一组要监视的输入源(键盘,触摸等)和一组要通知的观察者组成。

隐式或显式地使用特定的运行模式初始化运行循环。 在其生命周期内, 监视与该模式关联的源并允许其传递事件; 仅将与该模式关联的观察者通知新数据。

Cocoa / UIKit定义了几种类型的模式:在iOS上,有一种特殊的模式称为UITrackingRunLoopMode:在控件中进行跟踪时设置。 这是一个基本的部分,因为它可以平稳地呈现UI事件。 例如,在此模式下的拖动循环或其他用户界面跟踪循环中,此模式下的处理将限制输入事件。 例如,当手指在UITableView上拖动时将处于此模式。
虽然主线程的运行循环位于UITrackingRunLoopMode中,但大多数后台事件(如网络回调)并未传递。 并且不会进行任何额外的处理(这意味着在滚动过程中不会出现延迟)。

在检查事件的连续循环中,NSRunLoop还检查计时器间隔经过的事件。 一旦检测到,它将调用由NSTimer类注册的方法。

对NSTimers意味着什么? 这很简单; 通常,当用户滚动表或其他UIScrollView或执行将运行循环置于事件跟踪模式的其他操作时,它们不会触发

即使可以使用NSTimer,我也将获得一个健壮的替代方案,以至少避免使用此类的一些主要限制。

没有纯粹的Swift类

虽然这当然不是问题,但我们希望有一个不依赖于NSObject和Obj-C的Timer,在其中我们可以设置事件回调而不必担心保留周期。

不支持实时

这意味着NSTimer 不能用作实时计时器 :管理火灾事件的循环点可能会延迟,您可能会失去精度。
实际上,NSTimer和GCD计时器都不适合实时需求(您不能将它们用于对延迟敏感的用途,例如视频缓冲区同步或音频处理) ; 计时器的时间间隔的有效分辨率限制为50–100毫秒(提示:CADisplayLink)。

需要有效的运行循环

如您所见,NSTimer需要一个活动的运行循环。 在主线程中初始化时,它将自动使用主运行循环。 如果需要创建后台计时器,则需要将其附加到线程的运行循环并调用run()使其处于活动状态。

保留周期和线程问题

您还必须注意invalidate(); 您必须记住要调用它,否则Run循环会强烈引用计时器的目标对象,从而可能导致泄漏。
此外, 必须在创建计时器本身的同一线程中调用 invalidate()。

不能重复使用

另一个陷阱是, 您不能重用无效的计时器实例 ; 您将不得不分配一个新实例。 没有暂停,没有简历。

Grand Central Dispatch提供了一种在Apple平台上创建计时器的简便方法:DispatchSourceTimer。

我们的范围是使用它为我们的项目创建一个新的Timer类。 我们将其称为重复。 这个小项目的主要目标是:

  • 创建和管理计时器的简单,冗长的API方法。
  • 避免强烈引用目标目标,并避免NSObject继承。
  • 使用回调通知火灾事件; 允许多个观察者订阅同一计时器
  • 能够暂停,启动,恢复和重置我们的计时器。
  • 能够设置不同的重复模式(无限:按固定间隔的无限次射击,无限:按规则间隔的有限的一次射击,一次:自启动以来以指定间隔的单个射击事件)。

首先,我们要定义一种简单的方法来指示计时器的间隔; 我们可以保留DispatchTimeInterval,但我们也希望能够在原始类仅使用Int值的情况下设置一个浮动值以秒为单位。

我们的Repeat.Interval只是一个具有value属性的枚举,该枚举返回正确的DispatchInterval并允许我们指定时间单位的值:

该类的核心功能是configureTimer()函数,可在其中创建DispatchSourceTimer:

以下代码仅创建一个新的DispatchSourceTimer将其附加到给定的DispatchQueue (或新创建的),设置重复行为并设置回调以接收(请注意[weak self] )火灾事件。

为了接受多个观察者,我们定义了一个Dictionary,在其中存储每个新的观察者函数以及给定的唯一标识符。 触发后,将枚举字典并将事件分派给任何注册的观察者。
唯一的还可以用于删除现有的观察者。

以下方法允许注册/注销观察者:

我们还添加了暂停,启动/继续和重置功能。
正如我们所说的,GCD计时器可能有些敏感。
如果尝试恢复/暂停已经恢复/暂停的计时器,则会发生崩溃,并显示诸如“对象的过度恢复”之类的错误。 为了解决这个问题,我们只需要平衡挂起和恢复的调用即可。 每个方法都有一个安全的包装程序,以避免多次调用同一状态。

请记住:我们还应确保从同一线程/队列访问它,否则我们将需要添加一个内部串行队列以防止出现竞争情况。

如您所见, deinit()函数有点特殊:我们需要通过取消计时器实例来确保可以正确地释放它,否则它将调用一个释放对象。
我们也希望避免取消已暂停的计时器,因为这也会触发崩溃。

最后,我们要公开一些初始化快捷方式,例如:

  • every(_ interval: Interval, count: Int? = nil, handler: @escaping Observer)
  • once(after interval: Interval, _ observer: @escaping Observer)

轻松创建计时器,如下所示:

我们还可以添加其他观察者:

就这样!

和往常一样,完整的代码以及重复的单元测试都可以在我的github帐户上找到。 Repeat与所有Apple平台兼容,您可以将其与迦太基,cocoapods一起安装,也可以直接拖动项目的单个源代码文件来安装。
欢迎对本文和项目做出的所有贡献。