iOS-NSTimer的前世今生(NSTimer不同创建方式的区别)

时间:2022-05-09 20:33:48

前言

昨天有个小伙伴问我NSTimer有很多种创建方式,他们有什么区别吗?其实想想NSTimer8创建方式,但是总的说起来就三种timerWithTimeInterval、scheduledTimerWithTimeInterval和initWithFireDate,但是又细分起来就两种,一种是需要手动加入NSRunLoop,一种是自动加入NSRunLoop中。NSTimer的八种方法如下:

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

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

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

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

下面我会详细介绍这八种方法的!iOS-NSTimer的前世今生(NSTimer不同创建方式的区别)

方法介绍

1.timerWithTimeInterval

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

Interval:设置时间间隔,以秒为单位,一个>0的浮点类型的值,如果该值<0,系统会默认为0.1

target:表示发送的对象,如self

selector:方法选择器,在时间间隔内,选择调用一个实例方法

userInfo:此参数可以为nil,当定时器失效时,由你指定的对象保留和释放该定时器。

repeats:当YES时,定时器会不断循环直至失效或被释放,当NO时,定时器会循环发送一次就失效。

使用示例:

NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRequest) userInfo:nil repeats:YES];    [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSRunLoopCommonModes];- (void)timerRequest{    NSLog(@"定时器开始。。。");}

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block 

block:使用block的方法就直接在block里面写延时后要执行的代码就可以了

使用示例:

NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {        NSLog(@"定时器开始。。。");    }];    [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSRunLoopCommonModes];

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

invocation:需要执行的方法

使用示例:

NSMethodSignature *sgn = [self methodSignatureForSelector:@selector(timerRequest)];    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: sgn];    [invocation setTarget: self];    [invocation setSelector:@selector(timerRequest)];    NSTimer *timer3 = [NSTimer timerWithTimeInterval:1.0 invocation:invocation repeats:YES];    [[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes];

2.scheduledTimerWithTimeInterval

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

使用示例:

NSTimer *timer4 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerRequest) userInfo:nil repeats:YES];

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

使用示例:

NSTimer *timer5 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {        NSLog(@"定时器开始。。。");    }];

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

使用示例:

NSMethodSignature *sgn = [self methodSignatureForSelector:@selector(timerRequest)];    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: sgn];    [invocation setTarget: self];    [invocation setSelector:@selector(timerRequest)];    NSTimer *timer6 = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];

3.initWithFireDate

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;

使用示例:

NSTimer *timer7 = [[NSTimer alloc]initWithFireDate:[NSDate distantPast] interval:1.0 target:self selector:@selector(timerRequest) userInfo:nil repeats:YES];    [[NSRunLoop mainRunLoop]addTimer:timer7 forMode:NSDefaultRunLoopMode];

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

使用示例

NSTimer *timer8 = [[NSTimer alloc]initWithFireDate:[NSDate distantPast] interval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {        NSLog(@"定时器开始。。。");    }];

参数说明:

启动定时器
[NSDate distantPast];
停止定时器
[NSDate distantFuture];

4.总结

从上面的栗子我们可以看出,通过timerWithTimeInterval和initWithFireDate方法创建出来的定时器,都需要手动加入到RunLoop中才会执行,否则不会执行;但是通过scheduledTimerWithTimeInterval创建出来的定时器是自动加入到RunLoop,而且会自动执行。

成员变量

@property (copy) NSDate *fireDate;
这是设置定时器的启动时间,常用来管理定时器的启动与停止
启动定时器
timer.fireDate = [NSDate distantPast];
停止定时器
timer.fireDate = [NSDate distantFuture];

@property (readonly) NSTimeInterval timeInterval;
这个是一个只读属性,获取定时器调用间隔时间。


@property NSTimeInterval tolerance;
这是7.0之后新增的一个属性,因为NSTimer并不完全精准,通过这个值设置误差范围。

@property (readonly, getter=isValid) BOOL valid;
获取定时器是否有效

@property (readonly, retain) id userInfo;
获取参数信息

内存释放

如果我们创建一个定时器,在这个界面释放前,我们停止定时器或者置为nil,但是这样并不能释放定时器,因为我们把定时器自动或者手动添加到runloop中,所以系统的循环池中还有这个对象,并不能释放,所以我们应该手动将定时器从runloop中移除,[self.timer invalidate];然后再置为nil。

问题解答

1.什么是NSTimer

A timer provides a way to perform a delayed action or a periodic action. The timer waits until a certain time interval has elapsed and then fires, sending a specified message to a specified object. For example, you could create a timer that sends a message to a controller object, telling it to update a particular value after a certain time interval.
详细参考官方文档:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Timers/Timers.html#//apple_ref/doc/uid/10000061-SW1

2.NSTimer会准时触发事件吗

答案是否定的,而且有时候你会发现实际的触发时间跟你想象的差距还比较大。NSTimer不是一个实时系统,因此不管是一次性的还是周期性的timer的实际触发事件的时间可能都会跟我们预想的会有出入。差距的大小跟当前我们程序的执行情况有关系,比如可能程序是多线程的,而你的timer只是添加在某一个线程的runloop的某一种指定的runloopmode中,由于多线程通常都是分时执行的,而且每次执行的mode也可能随着实际情况发生变化。
假设你添加了一个timer指定2秒后触发某一个事件,但是签好那个时候当前线程在执行一个连续运算(例如大数据块的处理等),这个时候timer就会延迟到该连续运算执行完以后才会执行。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会和后面的触发进行合并,即在一个周期内只会触发一次。但是不管该timer的触发时间延迟的有多离谱,他后面的timer的触发时间总是倍数于第一次添加timer的间隙。
原文如下“A repeating timer reschedules itself based on the scheduled firing time, not the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.”
下面请看一个简单的例子:

- (void)SimpleExampleOne{    NSTimer *timer4 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerRequest) userInfo:nil repeats:YES];    self.timer = timer4;    [self performSelector:@selector(simulateBusy) withObject:nil afterDelay:3];}// 模拟当前线程正好繁忙的情况- (void)simulateBusy{    NSLog(@"start simulate busy!");    NSUInteger caculateCount = 0x0FFFFFFF;    CGFloat uselessValue = 0;    for (NSUInteger i = 0; i < caculateCount; ++i) {        uselessValue = i / 0.3333;    }    NSLog(@"finish simulate busy!");}

运行结果

iOS-NSTimer的前世今生(NSTimer不同创建方式的区别)

3.NSTimer为什么要添加到RunLoop中才会有作用

前面的例子中我们使用的是一种便利方法,它其实是做了两件事:首先创建一个timer,然后将该timer添加到当前runloop的default mode中。也就是这个便利方法给我们造成了只要创建了timer就可以生效的错觉,我们当然可以自己创建timer,然后手动的把它添加到指定runloop的指定mode中去。
NSTimer其实也是一种资源,如果看过多线程变成指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会又效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。你可能会说那我们APP的主线程的runloop我们没有往其中添加任何资源,为什么它还好好的运行。我们不添加,不代表框架没有添加,如果有兴趣的话你可以打印一下main thread的runloop,你会发现有很多资源。
下面我们看一个小例子:

- (void)SimpleExampleTwo{    NSLog(@"定时器开始创建");    NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRequest) userInfo:nil repeats:YES];    //没有把定时器添加到RunLoop中    //[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSRunLoopCommonModes];    self.timer = timer1;    NSLog(@"定时器创建完成");}
运行结果

iOS-NSTimer的前世今生(NSTimer不同创建方式的区别)

4.NSTimer加到了RunLoop中但迟迟的不触发事件

为什么明明添加了,但是就是不按照预先的逻辑触发事件呢???原因主要有以下两个:
a.runloop是否运行
每一个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动。
那么如果我们把一个timer添加到了非主线的runloop中,它还会按照预期按时触发吗?下面请看一段测试程序:
测试把timer加到不运行的runloop上的情况

- (void)SimpleExampleThree{    NSLog(@"定时器开始创建");    NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRequest) userInfo:nil repeats:YES];    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];    self.timer = timer1;    NSLog(@"定时器创建完成");}
运行结果

iOS-NSTimer的前世今生(NSTimer不同创建方式的区别)
解决办法:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{        //创建定时器    });

b.mode是否正确
我们前面自己动手添加runloop的时候,可以看到有一个参数runloopMode,这个参数是干嘛的呢?
前面提到了要想timer生效,我们就得把它添加到指定runloop的指定mode中去,通常是主线程的defalut mode。但有时我们这样做了,却仍然发现timer还是没有触发事件。这是为什么呢?
这是因为timer添加的时候,我们需要指定一个mode,因为同一线程的runloop在运行的时候,任意时刻只能处于一种mode。所以只能当程序处于这种mode的时候,timer才能得到触发事件的机会。
举个不恰当的例子,我们说兄弟几个分别代表runloop的mode,timer代表他们自己的才水桶,然后一群人去排队打水,只有一个水龙头,那么同一时刻,肯定只能有一个人处于接水的状态。也就是说你虽然给了老二一个桶,但是还没轮到它,那么你就得等,只有轮到他的时候你的水桶才能碰上用场。


转载请注明:http://blog.csdn.net/u014220518/article/details/70170006

本文参考:http://www.cnblogs.com/smileEvday/archive/2012/12/21/NSTimer.html