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 />
  1. + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
  2. + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

区别:

  • 后者除了创建一个定时器外会自动以NSDefaultRunLoopMode添加到当前线程RunLoop
  • 不添加到RunLoop中的NSTimer是无法正常工作的。

    生命周期

  • NSTimer 会对外界传递的target进行retain。如果是一次性调用(repeats:NO),会在本次调用之后自身invalidate,并且NSTimer retain的那个target会做一次release

  • 如果是多次重复调用,就需要我们自己手动进行invalidate,不然NSTimer一直存在。
  • NSTimer在那个线程创建就要在那个线程停止,否则资源不能正确的释放。

NSTimer循环引用问题

场景

对象:一个无限滚动的广告栏
image.png

方案一

image.png
代码如下:

  1. @interface TimerWeakObject : NSObject
  2. @property (nonatomic, weak) id target;
  3. @property (nonatomic, assign) SEL selector;
  4. @property (nonatomic, weak) NSTimer *timer;
  5. - (void)fire:(NSTimer *)timer;
  6. @end
  7. @implementation TimerWeakObject
  8. - (void)fire:(NSTimer *)timer
  9. {
  10. if (self.target) {
  11. if ([self.target respondsToSelector:self.selector]) {
  12. [self.target performSelector:self.selector withObject:timer.userInfo];
  13. }
  14. }else{
  15. [self.timer invalidate];
  16. }
  17. }
  18. @end
  19. @implementation NSTimer (WeakTimer)
  20. + (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
  21. target:(id)aTarget
  22. selector:(SEL)aSelector
  23. userInfo:(id)userInfo
  24. repeats:(BOOL)repeats
  25. {
  26. TimerWeakObject *object = [[TimerWeakObject alloc] init];
  27. object.target = aTarget;
  28. object.selector = aSelector;
  29. object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
  30. return object.timer;
  31. }
  32. @end

方案二

NSTimeriOS10中推出了两个新的API,与上面的方法比较区别主要是selector换成Block回调,不过要是适配低版本还是尽量使用上面的方法。两个API如下:

  1. + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval
  2. repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
  3. API_AVAILABLE(macosx(10.12),
  4. ios(10.0), watchos(3.0), tvos(10.0));
  5. + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:
  6. (BOOL)repeats block:(void (^)(NSTimer *timer))block
  7. API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

注:以下这个方式是iOS10才出现的,使用block来解决NSTimer循环引用的问题,暂时不考虑下面这种方法。

  1. __weak typeof(self) weakSelf = self;
  2. self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
  3. [weakSelf timerAction];
  4. }];

注意: block也会造成循环引用

对于UIScrollView的Timer

当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的RunLoop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到RunLoop中的Timer就不会执行。

解决方案:

  1. NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];
  2. //使用NSRunLoopCommonModes模式,把timer加入到当前RunLoop中。
  3. [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  4. 2. NSTimer放到新的线程中
  5. NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
  6. [thread start];
  7. - (void)newThread{
  8. @autoreleasepool{
  9. //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
  10. timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES];
  11. //开始执行新线程的Run Loop,如果不启动runloop,timer的事件是不会响应的
  12. [[NSRunLoop currentRunLoop] run];
  13. }
  14. }

总结

  • 创建NSTimer都是要加到Runloop中的,不管是手动添加还是系统添加,加入到Runloop中才能生效
  • 当Timer加入到runloop的模式的NSDefaultRunLoopMode,当UIScrollView滑动的时候会暂时失效
  • 注意timer在不需要时,一定要调用invalidate方法释放定时器
  • 使用NSTimer会存在延时,计时不是很准。因为不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,
    • 如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。
    • 重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行,这个延迟时间大概为50-100毫秒.
    • NSTimer不是绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就pass过去了.
    • 在对时间准确精度不要求特别高的话,使用NSTimer定时器。

参考