iOS 定时器
NSTimer 简单介绍
1 | // 创建方式一 |
方式一
创建定时器后需要手动将定时器添加到runLoop
,否则不会执行1
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
方式二
会自动将创建的定时器以默认模式添加到runLoop
中,当屏幕滚动时runLoop
则会进入另外一种模式TrackingRunLoopMode
,导致定时器暂停,所以一般我们也需要将其手动添加到NSRunLoopCommonModes
中1
NSRunLoopCommonModes = NSDefaultRunLoopMode + UITrackingRunLoopMode
对于重复定时器,必须通过调用其
invalidate
方法来自行使定时器对象无效。调用此方法,将定时器从当前运行循环中删除; 所以,调用invalidate
必须和构建定时器时,保持在同一线程中,这样就可以使计时器立即失效会并禁用,就不会再影响运行循环。
精准度
我们都知道NSTimer会有精度问题,但是是怎么造成的呢?
- 定时器计算下一个触发时间是根据初始触发时间计算的,下一次触发时间是定时器的整数倍+容差
tolerance
- 定时器是添加到
runloop
中的。如果runloop
阻塞了,调用或执行方法所花费的时间长于指定的时间间隔(第1点计算得到的时间,就会推迟到下一个runloop
周期。- 定时器是不会尝试补偿在调用或执行指定方法时可能发生的任何错过的触发。
内存泄漏
网上也有一些很好的解决方式:通过分类增加block,实现方式跟系统提供的一样
在博客里作者也很好的解释了循环引用问题
Runloop对定时源的观察者要进行保留以便时间点到了进行调用,即定时器对象被Runloop强保留着,而定时器内部持有控制器的target,控制器为了方便后续使用,又持有了定时器。这就造成了循环使用。
虽然系统提供了
invalidate
来是定时器失效,但是应该在哪里调用这个方法合适呢?delloc?不可能的了!
这里我介绍一种YYKit
里面的方法,利用消息转发实现的,这种方式相对方便,也适用在CADisplayLink
造成的内存泄漏
1 | // 创建一个继承自NSProxy的子类 |
- 使用:
1 | @implementation MyView { |
CADisplayLink
CADisplayLink 简单介绍
CADisplayLink
是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类
- 每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息,CADisplayLink类对应的selector就会被调用一次,所以可以使用CADisplayLink做一些和屏幕操作相关的操作。
- NSTimer 以指定的模式注册到 RunLoop 后,每当指定的时间到达后,RunLoop 会向指定的 target 发送一次指定的 selector 消息。
同NSTimer一样,也会因为同样的问题造成精度问题。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,造成界面卡顿的感觉。
- 创建方式
1 | CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(fire)]; |
CADisplayLink、NSTimer 都只能添加到一个runloop中,但是可以多模式
内存泄漏
与NSTimer
类似,会出现循环引用。
解决方法同NSTimer
1 | _link = [CADisplayLink displayLinkWithTarget:[ANWeakProxy proxyWithTarget:self] selector:@selector(fire)]; |
GCD
dispatch_source_t 的使用
关于GCD定时器探究,这里介绍一篇比较清晰的文章 iOS倒计时的探究与选择
1 | _requestTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); |
NSObject 中的performSelector
这种一次性的定时器相对于 GCD 中的 dispatch_after
,使用还是比较少的。
1 | - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes; |
与NSTimer scheduledTimerWithTimeInterval
类似,在内部会帮我们创建NSTimer的同时,添加到当前线程的runloop中
如果线程是主线程(程序一启动,就默认开启主线程的runloop),而我们创建的子线程的runloop默认是关闭的,不手动激活,而且基于NSTimer,精度问题也存在!