iOS-NSTimer不同创建方式的区别

先说总结

  • 创建NSTimer必须加入到Runloop中才能生效,不管是手动添加还是系统添加。
  • 当Timer加入到runloop的模式的NSDefaultRunLoopMode,当UIScrollView滑动的时候会暂时失效
    ** 疑问有UICrollView滑动执行执行的模式是UITrackingRunLoopMode,NSDefaultRunLoopMode被挂起了,会导致定时器失效,停止滑动时会执行NSDefaultRunLoopMode模式,才会恢复定时器。

解决方法是

//第一种给NSTimer分别添加到UITrackingRunLoopMode 和 NSDefaultRunLoopMode这两个模式中
[[NSRunLoop mainRunLoop] addTimer:timer 
forMode:NSDefaultRunLoopMode];
[[NSRunLoop mainRunLoop] addTimer:timer 
forMode: UITrackingRunLoopMode]; 
//第二种给NSTimer添加NSRunLoop的NSRunLoopCommonModes中,平常用这中就可以了
[[NSRunLoop mainRunLoop] addTimer:timer 
forMode: NSRunLoopCommonModes]; 
  • 使用NSTimer会存在延时,计时不是很准。因为不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关
    ** 如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。
    ** 重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行,这个延迟时间大概为50-100毫秒.
    ** NSTimer不是绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就pass过去了.
    ** 在对时间准确精度不要求特别高的话,使用NSTimer定时器。

NSTimer有8种创建方式,但是总的说起来就三种timerWithTimeInterval、scheduledTimerWithTimeInterval和initWithFireDate,但是又细分起来就两种,一种是需要手动加入NSRunLoop,一种是自动加入NSRunLoop中。NSTimer的八种方法如下:

// 以下三种方式创建一个定时器,但是没有添加到runloop,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法。
+ (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;
 
// 以下三种方式创建一个timer并把它指定到一个默认的runloop模式中,并且在 TimeInterval时间后 启动定时器 
+ (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;

方法介绍

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时,定时器会循环发送一次就失效。

- (void)createTimer {
    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里面写延时后要执行的代码就可以了

- (void)createTimer { 
    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:需要执行的方法

- (void)createTimer { 
    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;
- (void)createTimer { 
    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;
- (void)createTimer { 
     NSTimer *timer5 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"定时器开始。。。");
    }];
}
  • (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)createTimer { 
    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;
- (void)createTimer { 
    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;
- (void)createTimer { 
    NSTimer *timer8 = [[NSTimer alloc]initWithFireDate:[NSDate distantPast] interval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"定时器开始。。。");
    }];
}

方法说明:

//启动定时器
[timer setFireDate:[NSDate distantPast]];
timer.fireDate = [NSDate distantPast];
//停止定时器
[timer setFireDate:[NSDate distantFuture]];
timer.fireDate = [NSDate distantFuture];
//取消定时器
[timer invalidate];
timer = nil;

总结

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

成员变量

//这是设置定时器的启动时间,常用来管理定时器的启动与停止
@property (copy) NSDate *fireDate;
//这个是一个只读属性,获取定时器调用间隔时间。
@property (readonly) NSTimeInterval timeInterval;
//这是7.0之后新增的一个属性,因为NSTimer并不完全精准,通过这个值设置误差范围。
@property NSTimeInterval tolerance;
//获取定时器是否有效
@property (readonly, getter=isValid) BOOL valid;
//获取参数信息
@property (readonly, retain) id userInfo;

内存释放

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

问题解答

1.什么是NSTimer

iOS中最基本的定时器,其通过RunLoop来实现,一般情况下较为准确,但当当前循环耗时操作较多时,会出现延迟问题。同时,也受所加入的RunLoop的RunLoopMode影响,具体可以参考RunLoop的特性。

“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.”翻译过来就是timer就是一个能在从现在开始的后面的某一个时刻或者周期性的执行我们指定的方法的对象。
详细参考官方文档:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Timers/Timers.html#//apple_ref/doc/uid/10000061-SW1

2.NSTimer会准时触发事件吗

答案:是否定的,
NSTimer不准时的原因:
1:RunLoop循环处理的时间
2:受RunLoop模式的影响
而且有时候你会发现实际的触发时间跟你想象的差距还比较大。NSTimer不是一个实时系统,因此不管是一次性的还是周期性的timer的实际触发事件的时间可能都会跟我们预想的会有出入。差距的大小跟当前我们程序的执行情况有关系,比如可能程序是多线程的,而你的timer只是添加在某一个线程的runloop的某一种指定的runloopmode中,由于多线程通常都是分时执行的,而且每次执行的mode也可能随着实际情况发生变化。
假设你添加了一个timer指定2秒后触发某一个事件,但是签好那个时候当前线程在执行一个连续运算(例如大数据块的处理等),这个时候timer就会延迟到该连续运算执行完以后才会执行。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会和后面的触发进行合并,即在一个周期内只会触发一次。但是不管该timer的触发时间延迟的有多离谱,他后面的timer的触发时间总是倍数于第一次添加timer的间隙。

- (void)SimpleExampleOne{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerRequest) userInfo:nil repeats:YES];
    self.timer = timer;
    [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!");
}

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

NSTimer其实也是一种事件,而所有的source(事件)如果要起作用,必须添加到runloop中,并且此runloop是有效的,并运行着。
同理timer这种source(事件)要想起作用,那肯定也需要加到runloop中才会有效。
如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。你可能会说那我们APP的主线程的runloop我们没有往其中添加任何资源,为什么它还好好的运行。我们不添加,不代表框架没有添加,如果有兴趣的话你可以打印一下main thread的runloop,你会发现有很多资源。

下面我们看一个小例子:

(void)applicationDidBecomeActive:(UIApplication *)application
{
    [self testTimerWithOutShedule];
}

- (void)testTimerWithOutShedule
{
    NSLog(@"Test timer without shedult to runloop");
    SvTestObject *testObject3 = [[SvTestObject alloc] init];
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject3 selector:@selector(timerAction:) userInfo:nil repeats:NO];
    [testObject3 release];
    NSLog(@"invoke release to testObject3");
}

- (void)applicationWillResignActive:(UIApplication *)application
{
    NSLog(@"SvTimerSample Will resign Avtive!");
}

这个小例子中我们新建了一个timer,为它指定了有效的target和selector,并指出了1秒后触发该消息,运行结果如下:

image

观察发现这个消息永远也不会触发,原因很简单,我们没有将timer添加到runloop中。

综上: 必须得把timer添加到runloop中,它才会生效。

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

为什么明明添加了,但是就是不按照预先的逻辑触发事件呢???原因主要有以下两个:

1、runloop是否运行

每一个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动。

那么如果我们把一个timer添加到了非主线的runloop中,它还会按照预期按时触发吗?下面请看一段测试程序:

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [NSThread detachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:self withObject:nil];
}

// 测试把timer加到不运行的runloop上的情况
- (void)testTimerSheduleToRunloop1
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    NSLog(@"Test timer shedult to a non-running runloop");
    SvTestObject *testObject4 = [[SvTestObject alloc] init];
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject4 selector:@selector(timerAction:) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 打开下面一行输出runloop的内容就可以看出,timer却是已经被添加进去
    //NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]);
    
    // 打开下面一行, 该线程的runloop就会运行起来,timer才会起作用
    //[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    
    [testObject4 release];
    NSLog(@"invoke release to testObject4");

    [pool release];
}

- (void)applicationWillResignActive:(UIApplication *)application
{
    NSLog(@"SvTimerSample Will resign Avtive!");
}

上面的程序中,我们新创建了一个线程,然后创建一个timer,并把它添加当该线程的runloop当中,但是运行结果如下:

image

观察运行结果,我们发现这个timer知道执行退出也没有触发我们指定的方法,如果我们把上面测试程序中“//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];”这一行的注释去掉,则timer将会正确的掉用我们指定的方法。

2、mode是否正确

我们前面自己动手添加runloop的时候,可以看到有一个参数runloopMode,这个参数是干嘛的呢?

前面提到了要想timer生效,我们就得把它添加到指定runloop的指定mode中去,通常是主线程的defalut mode。但有时我们这样做了,却仍然发现timer还是没有触发事件。这是为什么呢?

这是因为timer添加的时候,我们需要指定一个mode,因为同一线程的runloop在运行的时候,任意时刻只能处于一种mode。所以只能当程序处于这种mode的时候,timer才能得到触发事件的机会。

综上: 要让timer生效,必须保证该线程的runloop已启动,而且其运行的runloopmode也要匹配。

你可能感兴趣的:(iOS-NSTimer不同创建方式的区别)