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也要匹配。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容

  • 定时器的用法 系统提供了8个创建方法,6个类创建方法,2个实例初始化方法。有三个方法直接将timer添加到...
    gpylove阅读 1,818评论 1 3
  • iOS中计时器常用的有两种方式 使用NSTimer类(Swift 中更名为 Timer) NSTimer 常用的初...
    superDg阅读 1,849评论 0 1
  • 一、NSTimer简介 二、NSTimer与RunLoop 三、NSTimer内存泄露分析1.NSTimer引用分...
    浮游lb阅读 5,685评论 2 23
  • 1.不开启RunLoop的线程在遇到一些耗时操作时,为了避免主线程阻塞导致界面卡顿,影响用户体验,往往我们会把这些...
    shinedada阅读 374评论 0 2
  • NSTimer 是系统提供的定时器,系统提供的api也比较简单,使用很方便,项目开发中会经常用到。然而,在使用NS...
    恋空K阅读 3,978评论 0 13