A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object.
timer
是一个能从某时刻或者周期性的给target对象发送一条指定的消息。
创建
timerWithTimeInterval
scheduledTimerWithTimeInterval<br />
+ (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
区别:
- 后者除了创建一个定时器外会自动以
NSDefaultRunLoopMode
添加到当前线程RunLoop
中 -
生命周期
NSTimer 会对外界传递的
target
进行retain
。如果是一次性调用(repeats:NO)
,会在本次调用之后自身invalidate
,并且NSTimer retain的那个target会做一次release
。- 如果是多次重复调用,就需要我们自己手动进行
invalidate
,不然NSTimer
一直存在。 - NSTimer在那个线程创建就要在那个线程停止,否则资源不能正确的释放。
NSTimer循环引用问题
场景
方案一
代码如下:
@interface TimerWeakObject : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
- (void)fire:(NSTimer *)timer;
@end
@implementation TimerWeakObject
- (void)fire:(NSTimer *)timer
{
if (self.target) {
if ([self.target respondsToSelector:self.selector]) {
[self.target performSelector:self.selector withObject:timer.userInfo];
}
}else{
[self.timer invalidate];
}
}
@end
@implementation NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats
{
TimerWeakObject *object = [[TimerWeakObject alloc] init];
object.target = aTarget;
object.selector = aSelector;
object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
return object.timer;
}
@end
方案二
NSTimer
在iOS10
中推出了两个新的API
,与上面的方法比较区别主要是selector换成Block回调,不过要是适配低版本还是尽量使用上面的方法。两个API如下:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
API_AVAILABLE(macosx(10.12),
ios(10.0), watchos(3.0), tvos(10.0));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:
(BOOL)repeats block:(void (^)(NSTimer *timer))block
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
注:以下这个方式是iOS10
才出现的,使用block来解决NSTimer循环引用的问题,暂时不考虑下面这种方法。
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerAction];
}];
注意: block也会造成循环引用
对于UIScrollView的Timer
当使用NSTimer的scheduledTimerWithTimeInterval
方法时。事实上此时Timer会被加入到当前线程的RunLoop中,且模式是默认的NSDefaultRunLoopMode
。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode
模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval
添加到RunLoop中的Timer就不会执行。
解决方案:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];
//使用NSRunLoopCommonModes模式,把timer加入到当前RunLoop中。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
2. 将NSTimer放到新的线程中
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
- (void)newThread{
@autoreleasepool{
//在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES];
//开始执行新线程的Run Loop,如果不启动runloop,timer的事件是不会响应的
[[NSRunLoop currentRunLoop] run];
}
}
总结
- 创建NSTimer都是要加到Runloop中的,不管是手动添加还是系统添加,加入到Runloop中才能生效
- 当Timer加入到runloop的模式的NSDefaultRunLoopMode,当UIScrollView滑动的时候会暂时失效
- 注意timer在不需要时,一定要调用
invalidate
方法释放定时器 - 使用NSTimer会存在延时,计时不是很准。因为不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,
- 如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。
- 重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行,这个延迟时间大概为50-100毫秒.
- NSTimer不是绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就pass过去了.
- 在对时间准确精度不要求特别高的话,使用NSTimer定时器。