首先看一下官方文档上对于NSTimer的介绍

创建

首先看一下创建NSTimer的方法,有以下几个,前面四个都是类方法,后面一个是实例方法。

+ scheduledTimerWithTimeInterval:invocation:repeats:

+ scheduledTimerWithTimeInterval:target:selector:userInfo:repeats

+ timerWithTimeInterval:invocation:repeats:

+ timerWithTimeInterval:target:selector:userInfo:repeats:

- initWithFireDate:interval:target:selector:userInfo:repeats:

在这些里面一般第一个和第三个不太常用,因为NSInvocation很少用到。


看一下第二个方法

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                     target:(id)target
                                   selector:(SEL)aSelector
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats                                

的具体说明:

Creates and returns a new NSTimer object and schedules it on the current run loop in the default mode.

After ti seconds have elapsed, the timer fires, sending the message aSelector to target.

即创建一个NSTimer对象,并把它以默认的模式加入到当前的run loop中。当ti时间过去后,触发定时器,发送消息给目标。

ti,表示从当前开始之后多长时间(以秒为单位)触发这个定时器。如果这个值小于或者等于0,那么默认是0.1毫秒。即,当使用scheduledTimerWithTimeInterval定时器创建之后加入了runloop中,不是立即触发的,也不需要调用fire方法触发,而是经过ti时间后才触发。

target,当定时器触发的时候,接收发送的消息的对象。The timer maintains a strong reference to target until it (the timer) is invalidated. 要注意,定时器会对target强引用除非定时器无效了。

aSelector,当定时器触发时,需要发送的消息。一般是建议timerFireMethod:这种形式带一个参数的,参数默认就是NSTimer,就像设置notification一样。但是也可以是不带参数的方法。

userInfo,这个参数可以是nil。NSTimer会对这个对象强引用触发定时器无效了。一般都默认给nil。

repeats,是否重复。如果是YES,则它会一直重复直到它无效。如果是NO,表示这个定时器是一次性的。

假如说创建了一个NSTimer,它负责打印一些东西

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(printNumber) userInfo:nil repeats:YES];

	NSLog(@"timer created");
}


-(void)printNumber{
    NSLog(@"number---");
    
}

NSTimer scheduleTimerWithTimeInterval

可以看到从定时器创建到发送指定消息,中间过了指定时间。


看一下第四个方法

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti
                            target:(id)target
                          selector:(SEL)aSelector
                          userInfo:(id)userInfo
                           repeats:(BOOL)repeats                              

的具体说明:

Creates and returns a new NSTimer object initialized with the specified object and selector.

You must add the new timer to a run loop, using addTimer:forMode:. Then, after ti seconds have elapsed, the timer fires, sending the message aSelector to target. (If the timer is configured to repeat, there is no need to subsequently re-add the timer to the run loop.)

即创建一个NSTimer对象。**【注意,并没有放入run loop中,需要在代码中手动把它加入到run loop中】**如果定时器是重复的,那么不需要重复添加到run loop中。


看最后一个方法:

- (instancetype)initWithFireDate:(NSDate *)date
                        interval:(NSTimeInterval)ti
                          target:(id)target
                        selector:(SEL)aSelector
                        userInfo:(id)userInfo
                         repeats:(BOOL)repeats

的具体说明:

You must add the new timer to a run loop, using addTimer:forMode:. Upon firing, the timer sends the message aSelector to target. (If the timer is configured to repeat, there is no need to subsequently re-add the timer to the run loop.)

即需要在代码中手动把它加入到run loop中。

其中date参数的含义是,该定时器第一次触发的时间。ti表示repeat情况下触发时间的间隔。

如果代码改成这样,date为当前时间即设置定时器创建的时候就触发:

self.timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:5 target:self selector:@selector(printNumber) userInfo:nil repeats:YES];
NSRunLoop * currentRunLoop = [NSRunLoop currentRunLoop];
[currentRunLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
NSLog(@"timer created");

那么打印结果如下:

NSTimer timerWithTimeInterval

如果date为过去的时间,那么结果如下,可以看到虽然初始化的时候给的第一次触发时间早于初始化的时间,但实际第一次触发的时间就是创建的时间,在此基础上才进行的第二次重复。(也就是说,我本来以为第一次打印的时间会是 9:38:52 - 0:0:5 + 0:0:15,实际上第一次打印的时间是9:38:52

self.timer = [[NSTimer alloc] initWithFireDate:[[NSDate alloc] initWithTimeIntervalSinceNow:-5] interval:15 target:self selector:@selector(printNumber) userInfo:nil repeats:YES];

timer past

如果date为未来的时间,那么结果为

self.timer = [[NSTimer alloc] initWithFireDate:[[NSDate alloc] initWithTimeIntervalSinceNow:5] interval:15 target:self selector:@selector(printNumber) userInfo:nil repeats:YES];

timer future

如果让NSTimer所在的线程休眠,那么结果如下,当定时器应该被触发的时候其所在线程正在休眠,当休眠结束后,会立即补上触发的定时器,而下次触发的时间是从原计划触发时间开始算的。(即,本来第一次触发的时间应该是10:13:41,但是这时候正在休眠,当10:13:43休眠结束,那么立即补上错过的那一次触发。下一次触发的时间是10:13:41(原定触发时间) + 0:0:15(间隔) = 10:13:56

self.timer = [[NSTimer alloc] initWithFireDate:[[NSDate alloc] initWithTimeIntervalSinceNow:5] interval:15 target:self selector:@selector(printNumber) userInfo:nil repeats:YES];
NSRunLoop * currentRunLoop = [NSRunLoop mainRunLoop];
[currentRunLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
NSLog(@"timer created");
[NSThread sleepForTimeInterval:7.0];

thread sleep

如果休眠的时间挺长,不仅错过了第一次触发,还错过了第二次触发呢?结果如下,休眠结束的时候错过了两次触发,本来应该是在10:21:0810:21:23的时候会有两次触发,但是这个时候线程在休眠,10:21:28休眠结束,那么补发最近一次错过的触发,但是更早之前错过的就不进行补发了。假如不休眠,按照原定计划,第三次触发应该是在10:21:38,事实也是这样。

self.timer = [[NSTimer alloc] initWithFireDate:[[NSDate alloc] initWithTimeIntervalSinceNow:5] interval:15 target:self selector:@selector(printNumber) userInfo:nil repeats:YES];
NSRunLoop * currentRunLoop = [NSRunLoop mainRunLoop];
[currentRunLoop addTimer:self.timer forMode:NSDefaultRunLoopMode]; 
NSLog(@"timer created");
[NSThread sleepForTimeInterval:25.0];

timer pass second firetime


addTimer:forMode

这个方法可以把NSTimer加入到runloop中,但有时候即使加入了runloop定时器也不会执行。

比如,下面,我让定时器每隔1秒就打印,期间我进行滑动操作,可以看到本该在13:53:01有一次定时器触发,但是结果没有。根据这篇博客所说,是mode的问题。

self.timer = [[NSTimer alloc] initWithFireDate:[[NSDate alloc] initWithTimeIntervalSinceNow:2] interval:1 target:self selector:@selector(printNumber) userInfo:nil repeats:YES];
NSRunLoop * currentRunLoop = [NSRunLoop mainRunLoop];
[currentRunLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];  
NSLog(@"timer created");

NSTimer scroll

定时器是以NSDefaultRunLoopMode这种模式加入到run loop中的,但是当滚动的时候,这个时候main run loop是处于NSEventTrackingRunLoopMode这种模式下,所以不会触发定时器,当滚动结束,定时器又可以正常触发了。

可以在addTimer:forMode的时候,设置定时器的模式为NSRunLoopCommonModes,即相当于NSDefaultRunLoopModeNSEventTrackingRunLoopMode的组合。那么在滚动的时候,定时器也是能触发的。


官网上有如下说明:

Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer.

当NSTimer被加入到run loop后,runLoop会对定时器进行强引用,所以你不需要自己再对定时器进行强引用。

NSTimer不是绝对准确的,可以精确到50-100毫秒。如果run loop不在运行,那么定时器也不会触发。


触发定时器

上面的几个方法是把NSTimer加入到runloop中,时机到了就会触发定时器。如果想要立即触发,那么可以使用fire方法.

self.timer = [[NSTimer alloc] initWithFireDate:[[NSDate alloc] initWithTimeIntervalSinceNow:5] interval:15 target:self selector:@selector(printNumber) userInfo:nil repeats:YES];
NSRunLoop * currentRunLoop = [NSRunLoop mainRunLoop];
[currentRunLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
NSLog(@"timer created");
    
[self.timer fire];


NSTimer fire

可以看到,当创建之后,立即就触发了定时器,然后按照创建时给定的触发时间和间隔时间进行触发。

假如是不循环的定时器,那么使用fire之后,就不会等到原定的时间才触发了。如果是只执行一次的定时器,那么执行完成之后就完成了。

停止定时器

使用invalidate可以停止一个定时器。官方文档的解释如下:

Stops the receiver from ever firing again and requests its removal from its run loop. This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point. If it was configured with target and user info objects, the receiver removes its strong references to those objects as well. You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly

这个方法是用来停止一个定时器,不管它是不是触发过,并且把它从run loop中去除。这是唯一一个可以把定时器从run loop去除的方法。从run loop去除之后,run loop就是释放对NSTimer的强引用,NSTimer也会释放对target和userInfo的强应用。注意,NSTimer在加入的是哪个run loop,就需要在该run loop里面向定时器发送invalidate消息。不然线程资源可能不能很好地释放。

进入后台,定时器是否会执行

一般应用,进入后台后,最长可以有10分钟的执行时间。在模拟器里面测试的时候,即使超过十分钟,还是可以运行的。在真机上测试的时候,很快NSTimer就不再运行了,当我重新进入前台的时候,系统会补发最近一次错过的定时器,然后又开始继续运行。

注意引用引起的内存问题

一般需求是在A页面使用定时器,让它每隔一段时间就重复某件事,当A页面释放的时候就停止定时器。一般会在A页面的dealloc里面加上[self.timer invalidate];,但是没用,还是在不断地执行。

这是因为NSTimer会对target强引用,所以target(也即是self)不会进入dealloc,所以定时器也就不会执行invaliddate方法。而且self会对成员变量进行引用,所以就造成了引用的循环。最终就是self没有释放,而且定时器也一直在工作。

一种解决办法就是不要将定时器停止的时机放在页面dealloc的时候,而改成一个按钮或者什么其他形式,可以手动停止计时器,使[self.timer invalidate];self.timer = nil;

另一种解决办法就是使用block。这种方法其实是一种迂回的办法,并没有从根儿上解决循环引用导致不能释放的问题,做法是:既然是会对target进行引用,那么把target里的self从类实例变成NSTimer,NSTimer可以看成是单例,这样的话viewController那个实例就可以释放了。

NSTimer block1

NSTimer block2

同时要注意,block里面如果对self发送消息,那么还是会造成NSTimer和viewController实例循环引用。因为block会对self强引用,而NSTimer会对userInfo强引用,相当于还是NSTimer强引用了viewController实例。所以在创建NSTimer的时候,在把block给NSTimer之前需要把强引用改成弱引用。

创建一个NSTimer的方法如下:

__weak typeof(self) weakSelf = self;
[NSTimer jg_scheduledTimerWithTimeInterval:3
                                     block:^{
                                         [weakSelf printNumber];
                                     }
                                   repeats:YES];

使用GCD实现定时器

@interface ViewController ()
@property (strong, nonatomic) dispatch_source_t gcdTimer;
@end

在实现里面这样:

@implementation ViewController
@synthesize timer = _timer;
- (void)viewDidLoad {
    [super viewDidLoad];    
    // 拿到一个队列
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);
    // 创建一个timer放到队列里
    self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);
    // 设置timer的首次执行时间、执行间隔、精确度
    dispatch_source_set_timer(self.gcdTimer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
    // 设置timer执行时的具体动作
    __weak typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(self.gcdTimer, ^{
        [weakSelf printNumber];
    });
    // 激活timer
    dispatch_resume(self.gcdTimer);
}

在必要的时候执行dispatch_source_cancel(self.gcdTimer);

这篇博客里面探究了一下GCD实现的定时器是不是会在某个情况下失效。值得一看。

参考

1.https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/

2.http://blog.callmewhy.com/2015/07/06/weak-timer-in-ios/

3.http://www.cnblogs.com/smileEvday/archive/2012/12/21/NSTimer.html

4.https://www.mgenware.com/blog/?p=459

5.http://www.jianshu.com/p/0c050af6c5ee

6.https://yq.aliyun.com/articles/17709