目录
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如何避免循环引用
其实我们在前面也稍稍讲过一些,总结下来有如下几种方法:
- 在ViewController即将消失时销毁定时器
- 对NSTimer进行二次封装
- iOS10之后的新API
- iOS10之前的API自己改造成block
- 使用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
使用时:
#import "ViewController.h"
#import "LHTimer.h"
@interface ViewController ()
@property (nonatomic, strong) LHTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Timer VC";
self.view.backgroundColor = [UIColor whiteColor];
self.timer = [[LHTimer alloc] init];
[timer startTimer];
}
- (void)dealloc
{
[self.timer destroyTimer];
}
这里的做法是将原来的timer和VC之间的强引用变成LHTimer和timer之间的强引用,避免timer直接强引用self代表的VC。然而,细心之下你还会发现两个问题:
- 二次封装提升代码的耦合度
- 即使封装也要将ti,target,selector,userInfo,repeat这些参数预留出来,供不同地方使用
3)iOS10之后的新API
+ (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));
新的API可以不用那么麻烦的去viewWillDisappear里释放,也不用封装,通过__weak弱引用self,可以避免循环引用,这时候就不是NSTimer的问题,而是block怎么避免循环引用了。
4)iOS10之前的API自己改造成block
iOS10之前的API,我们也经常将它封装成一个block形式的API,利用这种方式,可以达到上一条中的效果,当然,要做的事情肯定会多一些,为了详细说明,这里再贴下代码:
#import <Foundation/Foundation.h>
@interface NSTimer (LHTimer)
+ (NSTimer *)LH_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats;
@end
#import "NSTimer+LHTimer.h"
@implementation NSTimer (LHTimer)
+ (NSTimer *)LH_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats
{
return [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(handle:) userInfo:[block copy] repeats:repeats];
}
+ (void)handle:(NSTimer *)timer
{
void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
注意事项:
- copy是为防止block在需要的时候已经销毁掉,所以需要拷贝到堆上
- 使用时记得用__weak避免循环引用
- 使用该方案需要引入此类头文件
- 这是一个categroy,categroy可以给系统类添加新方法
5)使用NSProxy增加一个中间层subTarget
其原理是利用NSProxy给NSTimer加入一层stubTarget,stubTarget主要做为一个桥接层,负责NSTimer和target之间的通信。同时NSTimer强引用stubTarget,而stubTarget弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。
这个方式的好处是,当NSTimer的回调函数fireProxyTimer:
被执行的时候,会自动判断原target是否已经被释放,如果释放了,意味着NSTimer已经无效,虽然如此,但还是要在不需要的地方进行invalidate操作,只是不需要置nil。
下面看代码:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LHProxy : NSObject
//通过实例方法创建对象
- (instancetype)initWithObjc:(id)object;
//通过类方法创建对象
+ (instancetype)proxyWithObjc:(id)object;
@end
NS_ASSUME_NONNULL_END
#import "LHProxy.h"
@interface LHProxy()
@property (nonatomic, weak) id subTarget;
@end
@implementation LHProxy
- (instancetype)initWithObjc:(id)object {
self.subTarget = object;
return self;
}
+ (instancetype)proxyWithObjc:(id)object {
return [[self alloc] initWithObjc:object];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
if ([self.subTarget respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:self.subTarget];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.subTarget methodSignatureForSelector:sel];
}
@end
调用:
#import "ViewController1.h"
#import "LHProxy.h"
@interface ViewController1 ()
@property (nonatomic, strong)NSTimer *timer;
@end
@implementation ViewController1
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor yellowColor];
LHProxy *proxy = [[LHProxy alloc] initWithObjc:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(action) userInfo:nil repeats:YES];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissAction)];
[self.view addGestureRecognizer:tap];
}
- (void)dismissAction {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)action {
NSLog(@"1111111");
}
- (void)dealloc {
[self.timer invalidate];
}
@end
这里是将timer的target对象转移到其他对象,避免真正使用timer的对象被timer强引用(其实源头是被runloop强引用着)。这时,可以在真正使用timer对象的dealloc方法中调用timer的invalidate方法,来解除runloop对timer的强引用,进而释放timer对象。这一点和NSTimer的block用法类似。
总结:NSTimer的问题是在什么时候对其进行invalidate和置nil的问题,普通的NSTimer,放在dealloc内处理,因为self被NSTimer强引用,需要等到NSTimer被invalidate后才能释放,继而执行dealloc方法,而NSTime的invalidate方法则在dealloc内,这就造成了相互等待,无法释放的问题。掌握了这一点,NSTimer就不再再造成内训泄露和循环引用了。这里要格外注意一点,不管是哪种方式,最终都要调用invalidate方法,我们封装或者改造过的NSTimer只是可以不用做置nil操作,同时,可以写在dealloc内。