NSTimer定时器

NSTimer类详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface NSTimer : NSObject
+ (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;

- (void)fire;

@property (copy) NSDate *fireDate;
@property (readonly) NSTimeInterval timeInterval;

@property NSTimeInterval tolerance NS_AVAILABLE(10_9, 7_0);

- (void)invalidate;
@property (readonly, getter=isValid) BOOL valid;

@property (nullable, readonly, retain) id userInfo;

@end

关于创建NSTimer对象:

Use the scheduledTimerWithTimeInterval:invocation:repeats: or scheduledTimerWithTimeInterval: target:selector:userInfo:repeats: class method to create the timer and schedule it on the current run loop in the default mode.

Use the timerWithTimeInterval:invocation:repeats: or timerWithTimeInterval: target:selector:userInfo:repeats: class method to create the timer object without scheduling it on a run loop. (After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object.)

Allocate the timer and initialize it using the initWithFireDate:interval: target:selector:userInfo:repeats: method. (After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object.)

其他特性:

  • 触发:- (void)fire;
  • 开始时间:NSDate *fireDate;
  • 延迟时间:NSTimeInterval timeInterval;以秒为单位,一个>0的浮点类型的值,如果该值<0,系统会默认为0.1
  • 7.0之后新增的一个属性,NSTimer并不完全精准,通过这个值设置误差范围:NSTimeInterval tolerance;
  • 停止并删除 :- (void)invalidate;
  • 判断是否valid:BOOL valid;
  • 携带的用户信息:id userInfo;

其中fireDate也常用来管理定时器的启动与停止:,如下:

​ //启动定时器
​ self.timer.fireDate = [NSDate distantPast];
​ //停止定时器
​ self.timer.fireDate = [NSDate distantFuture];

另外停止也常用下面代码:

停止一般方法:
    if ([self.timer isValid] == YES) {
        [self.timer invalidate];
        self.timer = nil;
    }
或者
    if (self.timer) {
        [self.timer invalidate];
        self.timer =  nil;
    }

要注意的是:

  • scheduledTimerWithTimeInterval:invocation:repeats无需添加到main runloop并调用fire方法,运行循环的模式为默认模式NSDefaultRunLoopMode;
  • 其他方法创建对象则需要添加到runloop([[NSRunLoop mainRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode]),且开启定时器需要调用fire方法;
  • repeat:YES,需要手动停止定时器,repeat:NO,不需要手动,系统在执行定时器一次后,会自动停止该定时器;
  • NSTimer可以精确到50-100毫秒。并不绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就pass过去了,关于其循环周期:

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.

在下面创建NSTimer时,需要获取runloop,并加入到当前runloop中,

NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(jsp_doPull) userInfo:nil repeats:YES];
//加入主循环池中
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//开始循环
[timer fire];

需要说明的是,runloop这个概念:

  • runloop:程序运行循环,监听各种事件源和响应。
  • 运行循环模式,过滤感兴趣事件的“过滤器”:

    1. Default mode(NSDefaultRunLoopMode)
      默认模式中几乎包含了所有输入源(NSConnection除外),一般情况下应使用此模式。
    2. Connection mode(NSConnectionReplyMode)
      处理NSConnection对象相关事件,系统内部使用,用户基本不会使用。
    3. Modal mode(NSModalPanelRunLoopMode)
      处理modal panels事件。
    4. Event tracking mode(UITrackingRunLoopMode)
      在拖动loop或其他user interface tracking loops时处于此种模式下,在此模式下会限制输入事件的处理。例如,当手指按住UITableView拖动时就会处于此模式。
    5. Common mode(NSRunLoopCommonModes)
      这是一个伪模式,其为一组run loop mode的集合,将输入源加入此模式意味着在Common Modes中包含的所有模式下都可以处理。在Cocoa应用程序中,默认情况下Common Modes包含default modes,modal modes,event Tracking modes.可使用CFRunLoopAddCommonMode方法想Common Modes中添加自定义modes。

假如你的定时器没有工作?

你定义了一个定时器,指定了一个方法,可遗憾的是,它时钟没有被调用。

你需要检查一下:

  1. 有没有将定时器加入到runloop中,且该runloop是否已将run起来了;
  2. 是否runMode指定了所需要的模式;
  • 关于第一点,每个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动。run的方法:

    • (void)run;
    • (void)runUntilDate:(NSDate *)limitDate;
    • (BOOL)runMode:(NSString )mode beforeDate:(NSDate )limitDate;
  • 关于第二点,有一种常见的情况是:主界面有UITableView或者UIScrollView,滑动UITableView或者UIScrollView后NSTimer失效了。

原因在于,滑动时候,runMode处于UITrackingRunLoopMode模式,假如,我们的NSTimer对象是以非UITrackingRunLoopMode添加到runloop中的,那么定时器此时就毫无作用了。解决方法就是将其加入到UITrackingRunLoopMode模式或NSRunLoopCommonModes模式中。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

当然,你还有一个一劳永逸的方法,就是将该定时器添加到其他线程中,而不是主线程中:

1
2
3
4
dispatch_async(dispatch_get_global_queue(0, 0), ^{
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
});

NSTimer的销毁

利用系统的NSTimer类,会造成循环引用。通过分类(代码可见参考2.):

1
2
3
4
5
#import <Foundation/Foundation.h>
@interface NSTimer (Blocks)
+(id)scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats;
+(id)timerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import "NSTimer+Blocks.h"
@implementation NSTimer (Blocks)
+(id)scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats
{
void (^block)() = [inBlock copy];
id ret = [self scheduledTimerWithTimeInterval:inTimeInterval target:self selector:@selector(jsp_ExecuteBlock:) userInfo:block repeats:inRepeats];
return ret;
}

+(id)timerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats
{
void (^block)() = [inBlock copy];
id ret = [self timerWithTimeInterval:inTimeInterval target:self selector:@selector(jsp_ExecuteBlock:) userInfo:block repeats:inRepeats];
return ret;
}

+(void)jsp_ExecuteBlock:(NSTimer *)inTimer;
{
if([inTimer userInfo])
{
void (^block)() = (void (^)())[inTimer userInfo];
block();
}
}

@end

使用的话:

@interface NSTimerTest : NSObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (void)startPolling;
- (void)stopPolling;

@end

#import "NSTimerTest.h"
#import "NSTimer+Block.h"

@interface NSTimerTest() {
NSTimer *_pollTimer;
}

@end

@implementation NSTimerTest

- (instancetype)init {
if (self = [super init]) {

}
return self;
}

- (void)dealloc {
[_pollTimer invalidate];
_pollTimer = nil;
}

- (void)startPolling {

__weak typeof(self) weakSelf = self;
_pollTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 block:^{
NSTimerTest *strongSelf = weakSelf;
[strongSelf jsp_doPull];
} repeats:YES];

}

- (void)stopPolling {

}

- (void)jsp_doPull {

}

@end

当然,你可以参考GCD方法实现的NSTimer,见参考 3。

参考

  1. NSTimer Class Reference
  2. NSTimer-Blocks
  3. MSWeakTimer
  4. How do I use NSTimer?
  5. Timer Programming Topics