IOS定时器那点破事(一)

4,293 阅读5分钟

『一. 基本概念』

上面两种方式可以实现重复定时触发事件,但是target-action方式会存在一个问题?那就是对象之间的引用问题导致内存泄露,因为定时器强引用了self,而本身又被runloop强引用。所以timer和self都得不到释放,所以定时器一直存在并触发事件,这样就会导致内存泄露。

为了避免内存泄露,所以需要在不使用定时器的时候,手动执行timer.invalidate()方法。而block方式虽然并不会存在循环引用情况,但是由于本身被runloop强引用,所以也需要执行timer.invalidate()方法,否则定时器还是会一直存在。

invalidate方法有2个功能:一是将timer从runloop中移除,二是timer本身也会释放它持有的资源

timer.invalidate()
timer = nil

定时容忍范围(Timer Tolerance)

在iOS7之后,iOS允许我们为Timer指定Tolerance,这样会给你的timer添加一些时间宽容度可以降低它的电力消耗以及增加响应。好比如:“我希望1秒钟运行一次,但是晚个200毫秒我也不介意”。

当你指定了时间宽容度,就意味着系统可以在原有时间附加该宽容度内的任意时刻触发timer。例如,如果你要timer1秒后运行,并有0.5秒的时间宽容度,实际就可能是1秒,1.5秒或1.3秒等。

与Run Loop协同工作

当使用下列方法创建timer,需要手动添加timer到Run Loop并指定运行模型,上面使用的方法都是自动添加到当前的Run Loop并在默认模型(default mode)允许

// 手动添加到runloop,指定模型
func addTimerToRunloop() {
    let timer = Timer(timeInterval: 1.0,
                            target: self,
                          selector: #selector(fireTimer),
                          userInfo: nil,
                           repeats: true)
   RunLoop.current.add(timer, forMode: .common)
}

『二. 定时器的循环引用』

场景: 有两个控制器ViewControllerA和ViewControllerB,ViewControllerA 跳转到ViewControllerB中,ViewControllerB开启定时器,但是当返回ViewControllerA界面时,定时器依然还在走,控制器也并没有执行deinit方法销毁掉

为何会出现循环引用的情况呢?原因是:定时器对控制器 (self) 进行了强引用,定时器被runloop引用,定时器得不到释放,所以控制器也不会被释放

为了解决这个问题,有两种方法

方式1:

苹果官方为了给我们解决对象引用的问题,提供了一个新的定时器方法,利用block来解决与视图控制器的引用循环,但是只适用于iOS10和更高版本:

方式2:

既然Apple为我们提供了block方式解决循环引用问题,我们也可以模仿Apple使用block来解决,扩展Timer添加一个新方法来创建Timer

『三. 定时器的精确』

一般情况下使用Timer是没什么问题,但是对于精确到要求较高可以使用CADisplayLink(做动画)和GCD,对于CADisplayLink不了解,可以看CADisplayLink的介绍,对于定时器之间的比较,可以看更可靠和高精度的 iOS 定时器

定时器不准时的原因

  • 定时器计算下一个触发时间是根据初始触发时间计算的,下一次触发时间是定时器的整数倍+容差tolerance
  • 定时器是添加到runloop中的,如果runloop阻塞了,调用或执行方法所花费的时间长于指定的时间间隔(第1点计算得到的时间,就会推迟到下一个runloop周期。
  • 定时器是不会尝试补偿在调用或执行指定方法时可能发生的任何错过的触发
  • runloop的模式影响

『四. 利用GCD实现一个好的定时器』

而众所周知的是,NSTimer有不少需要注意的地方。

  1. 循环引用问题
    NSTimer会强引用target,同时RunLoop会强引用未invalidate的NSTimer实例。 容易导致内存泄露。
    (关于NSTimer引起的内存泄露可阅读iOS夯实:ARC时代的内存管理 NSTimer一节)
  2. RunLoop问题
    因为NSTimer依赖于RunLoop机制进行工作,因此需要注意RunLoop相关的问题。NSTimer默认运行于RunLoop的default mode中。
    而ScrollView在用户滑动时,主线程RunLoop会转到UITrackingRunLoopMode。而这个时候,Timer就不会运行,方法得不到fire。如果想要在ScrollView滚动的时候Timer不失效,需要注意将Timer设置运行于NSRunLoopCommonModes。
  3. 线程问题
    NSTimer无法在子线程中使用。如果我们想要在子线程中执行定时任务,必须激活和自己管理子线程的RunLoop。否则NSTimer是失效的。
  4. 不支持动态修改时间间隔
    NSTimer无法动态修改时间间隔,如果我们想要增加或减少NSTimer的时间间隔。只能invalidate之前的NSTimer,再重新生成一个NSTimer设定新的时间间隔。
  5. 不支持闭包。
    NSTimer只支持调用selector,不支持更现代的闭包语法。

『五. 后台定时器继续运行』

苹果上面的App一般都是不允许在后台运行的,比如说:定时器计时,当用户切换到后台,定时器就被被挂起,等回到App之后,才会Resume。

但是任何的app都能够使用 UIApplication background tasks在后台运行一小段时间,除此之外没有其他的办法。

『六. 定时器的暂停/开始』

NSTimer 暂停

 [timer setFireDate:[NSDate distantFuture]];

NSTimer 继续

 [timer setFireDate:[NSDate date]];