目录
1.什么是NSTimer?
2.NSTimer和RunLoop的关系
3.定时器释放的方式
4.NSTimer的时间准确吗?
5.NSTimer的衍变之路
6.NSTimer如何避免循环引用
1)在ViewController即将消失时销毁定时器
2)对NSTimer进行二次封装
3)iOS10之后的新API
4)iOS10之前的API自己改造成block
5)使用NSProxy增加一个中间层subTarget
1.什么是NSTimer?
NSTimer是一个定时器,是一个面向对象的定时器。在经过一定的时间间隔后触发,向目标对象发送指定的消息。其工作原理是将一个监听加入到系统的runloop中去,当系统runloop执行到timer条件的循环时,会调用timer一次,如果是一个重复的定时器,当timer回调函数runloop之后,timer会再一次的将自己加入到runloop中去继续监听下一次timer事件。
2.NSTimer和RunLoop的关系
前面已经说过,NSTimer的原理是将定时器中的事件添加到runloop中,以实现循环的,这是因为定时器默认处于runloop中的kCFRunLoopDefaultMode,主线程默认也处于此mode下,定时器这才具备了这样的能力。所以,没有runloop,NSTimer完全无法工作。
这里提出一个经典的案例:定时器默认无法在页面滚动时执行。
原因是滚动时,主线程runloop处于UITrackingRunLoopMode,这时候,定时器所处runloop依然处于kCFRunLoopDefaultMode,就导致定时器线程被阻塞,要解决这一个问题,我们就需要定时器无论是在kCFRunLoopDefaultMode还是UITrackingRunLoopMode下都可以正常工作,这时候就需要用到runloop中的伪模式kCFRunLoopCommonMode,这并不是一个真正的mode,而是一种多mode的处理方式。具体做法如下:
//创建定时器 _timer=[NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES]; //添加到循环中 NSRunLoop *runloop=[NSRunLoop currentRunLoop]; [runloop addTimer:_timer forMode:NSRunLoopCommonModes];
3.定时器释放的方式
[_timer invalidate]; _timer = nil;
二者缺一不可。
如果是在VC中创建的NSTimer,这种情况下,self和_timer相互强引用,VC的Delloc方法不会执行,所以定时器的销毁方法不能放在Delloc中,需要放在viewWillDIsappear中,原因我们放到最后说明。
4.NSTimer的时间准确吗?
不准确!NSTimer不是采用实时机制!
NSTimer的精确度略低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。
话都说到这里了,那肯定会有一种相对准确的方法,是的,CADisplayLink,但是其使用场景相对单一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。
CADisplayLink其原理更为复杂,因为内部操作的是一个source,CADisplayLink也并非百分百准确,当在两次屏幕刷新之间执行了一个长任务时,就会有一帧被跳过去,这一点倒是和NSTimer相似。
还有另一种定时器DispatchSourceTimer,这里不再赘述,它的准确度也是要高于NSTimer的,适用于对精确度要求相对较高的场景。如果做秒杀的计时器,推荐这种方式来做。
5.NSTimer的衍变之路
iOS10以前:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
iOS10以后:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
区别是有无Block的回调方法,block的作用就是将自身作为参数传递给block,来帮助避免循环引用,且使用起来更便捷,但要注意__weak和__strong的使用。
虽然如此,但还是有相当一部分人喜欢iOS10之前的API。
6.NSTimer如何避免循环引用
其实我们在前面也稍稍讲过一些,总结下来有如下几种方法:
1、在ViewController即将消失时销毁定时器
2、对NSTimer进行二次封装
3、iOS10之后的新API
4、iOS10之前的API自己改造成block
5、使用NSProxy增加一个中间层subTarget
1)在ViewController即将消失时销毁定时器
[_timer invalidate]; _timer = nil;
由于VC对_timer的强引用导致VC在销毁时Delloc方法无法执行,所以需要将销毁方法移步ViewWillDIsappear执行。
2)对NSTimer进行二次封装
#import <Foundation/Foundation.h> @interface LHTimer : NSObject //创建定时器 - (void)startTimer; //销毁定时器 - (void)destroyTimer; @end
#import "LHTimer.h" @implementation LHTimer { NSTimer *_timer; } - (void)startTimer { _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES]; } - (void)destroyTimer { if (!_timer) { return; } [_timer invalidate]; _timer = nil; } - (void)timerAction { NSLog(@"timerAction"); } - (void)dealloc { [_timer invalidate]; _timer = nil; } @end