【发烂渣】Swift-GCD定时器挂起(suspend)的奇怪现象

1,848 阅读4分钟

对于使用GCD的好处,大家可能在别的地方已经看到了和了解了。而如何使用GCD创建定时器也很简单。可是今天我要说的不是说如何创建的问题。而是在GCD定时器在使用上的一些奇怪现象!

简单的回顾:

1. 创建GCD定时器的代码如下:

var gcdTimer:DispatchSourceTimer?

func createGCDTimer()  {
        gcdTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global(qos: .default))
        gcdTimer?.schedule(deadline: .now(), repeating: .seconds(1), leeway: .nanoseconds(0));
        gcdTimer?.setEventHandler(handler: {
            printXY("fire")
        })
        gcdTimer?.resume()
    }

2. GCD提供的挂起、恢复、取消函数如下:

挂起函数:即为暂停定时器
gcdTimer?.suspend()

恢复函数:恢复和开始都调用该函数
gcdTimer?.resume()

取消函数:你可以理解为停止或者是销毁定时器
gcdTimer?.cancel()

可当你以为GCD定时器就这么简单,如此容易就完整某些功能的时候,请注意一下,坑可能已经悄悄的挖好了。

奇怪现象:

1. suspend()不可以对timer进行nil的设置

这样操作后,会得到一个报错信息:Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

  • 原因:简单来说,就是GCD的文档里面写了suspend()和resume()这两个函数需要配对使用.而当你挂起了定时器后,系统会记录挂起数,如果挂起数不为0,强行设置nil就会出现异常了。

  • 解决办法:添加一个变量去标志当前是否已经暂停了。因为GCD中并没有提供任何方法去获取当前定时器的状态.

2. 调用suspend(),再调用resume()会发现,重新恢复后eventHandler的前两次调用的时间间隔是错误的

  • 首先上我的测试代码
var gcdTimer:DispatchSourceTimer?
var count = 0

override func viewDidLoad() {
     super.viewDidLoad()
        
      gcdTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global(qos: .default))
      gcdTimer?.schedule(deadline: .now(), repeating: .seconds(1), leeway: .nanoseconds(0));
      gcdTimer?.setEventHandler(handler: {
          self.count += 1
          printXY("eventHandler \(self.count)")
      })
      gcdTimer?.resume()
    }
    
@IBAction func pauseC(_ sender: Any) {
    gcdTimer?.suspend()
}
    
@IBAction func resumeC(_ sender: Any) {
   gcdTimer?.resume()        
}

代码很简单,简单描述一下:

  1. 就是在viewDidLoad中创建了一个间隔为1秒的的定时器。然后立即开启定时器。
  2. EventHandler: 输出当前调用了多少次
  3. pauseC : 暂停函数的按钮事件
  4. resumeC: 恢复函数的按钮事件

我们来看看定时器刚刚开启的时候的输出,还是很正常,基本上就是一秒一个输出。如图:

2021-03-23 14:21:26.840 eventHandler 1
2021-03-23 14:21:27.840 eventHandler 2
2021-03-23 14:21:28.840 eventHandler 3
2021-03-23 14:21:29.839 eventHandler 4
2021-03-23 14:21:30.840 eventHandler 5
2021-03-23 14:21:31.840 eventHandler 6
2021-03-23 14:21:32.840 eventHandler 7
2021-03-23 14:21:33.839 eventHandler 8
2021-03-23 14:21:34.840 eventHandler 9
2021-03-23 14:21:35.840 eventHandler 10
2021-03-23 14:21:36.840 eventHandler 11
2021-03-23 14:21:37.839 eventHandler 12
2021-03-23 14:21:38.840 eventHandler 13

可在我调用了suspend()后,再调用resume()后,会发现在重新恢复的时候,会发现第1次和第2次的间隔有问题。如图:

2021-03-23 14:21:38.840 eventHandler 13
2021-03-23 14:21:39.752 suspend
2021-03-23 14:21:43.518 resume
2021-03-23 14:21:43.519 eventHandler 14
2021-03-23 14:21:43.840 eventHandler 15
2021-03-23 14:21:44.841 eventHandler 16
2021-03-23 14:21:45.840 eventHandler 17
2021-03-23 14:21:46.840 eventHandler 18
2021-03-23 14:21:47.841 eventHandler 19
2021-03-23 14:21:48.840 eventHandler 20

注意第14次输出和第15次输出,他们之间的间隔是不到一秒的。

2021-03-23 14:21:43.519 eventHandler 14
2021-03-23 14:21:43.840 eventHandler 15

这就很尴尬了。如果你是在做一个倒计时的功能或者其他需求的时候,因为某种原因导致了暂停再恢复的操作,就会出现一个时间加速的效果

解决方案:

而对于这个现象,我的解决方法是,

  • 不使用GCD提供的suspend()。
  • 暂停的功能调用cancel()函数。
  • 而在调用resume()进行恢复的时候,判断timer是否存在,存在则直接调用resume(),不存在则重新实例化定时器,再调用resume()

具体的代码可以参考我的Github代码,因为代码有点多。大家可以移步到这里