NSTimer在使用中需要注意的点

NSTimer的常用API
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
  Initializes a timer object with the specified object and selector.
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
  Creates a timer and schedules it on the current run loop in the default mode.

根据官方文档方法一和方法二的区别在于:使用方法一创建的Timer不会添加到NSRunLoop需要手动添加;使用方法二会自动添加到主线程的RunLoop中。

- (void)fire; 
  Causes the timer's message to be sent to its target.
- (void)invalidate;
  Stops the timer from ever firing again and requests its removal from its run loop.

fire方法可以立即触发Timer对象的target方法;invalidate会停止Timer并将其从runloop中移除

NSRunLoop & NSTimer

当使用NSTimerscheduledTimerWithTimeInterval方法时,NSTimer的实例会被加入到当前线程的RunLoop中,模式为默认模式NSDefaultRunLoopMode

- (void)viewDidLoad
{
    [super viewDidLoad];
        
    NSLog(@"主线程 %@", [NSThread currentThread]);
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    //使用NSRunLoopCommonModes模式,把timer加入到当前Run Loop中。
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)doSomething
{
    NSLog(@"Timer %@", [NSThread currentThread]);
}

控制台输出结果:上下打印出来的线程都是主线程。

如果当你线程是主线程也就是UI线程时,某些UI事件(UIScrollView的滑动操作),会将RunLoop切换到NSEventTrackingRunLoopMode模式。
在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到RunLoop中的Timer就不会执行。这是一个在开发中经常碰到的场景,如何解决这个问题?我们可以在创建完一个Timer后使用NSRunLoopaddTimer:forMode:方法,使用NSRunLoopCommonModes。这是一个占位用的Mode,可以在不同情况下扮演不同的角色——NSDefaultRunLoopMode & UITrackingRunLoopMode

NSTimer循环引用问题

NSTimer为什么会造成循环引用?
在开发中我们经常将Timer作为控制器的属性来使用,这样一来控制器对Timer进行了强引用。在target-action这个过程中,Timer又对self做了强引用,这就是导致循环引用的原因了。在网上有非常多的解决方案,我总结了一下有下面几种。

  • 方法一:在viewWillDisappear中执行下面的操作。

    - (void)viewWillDisappear:(BOOL)animated {
      [super viewWillDisappear:animated];
    
      [_timer invalidate];
      _timer = nil;
    }
    
     -- 
     苹果文档中关于target强引用的解释
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
      Parameters
      ti 
      The number of seconds between firings of the timer. If ti is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead.
      target 
      The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
    

The timer maintains a strong reference to this object until it (the timer) is invalidated.苹果文档说invalidate以后timer就不再保有对target的强引用了。所以解决循环引用的关键在与invalidate方法有没有执行。下面_timer = nil这句话的意义是,invalidate方法执行以后Timer就不能复用了,为了防止在其之后其他地方再次使用Timer,在这里即使将其置为nil。

  • 方法二:引入一个代理对象,让其弱引用self,参考YYWeakProxy
    没引入代理(Proxy)之前是:self -> timer -> self这样的循环引用。在引入代理(Proxy)之后是:self -> timer -> proxy ··> self。这样一来就打破了之前的循环引用。

  • 方法三:使用YYTimerMSWeakTimer等GCD实现的Timer。
    我决定把这一部分内容拿出来单独写一篇对比GCD、CADisplayLink等计时器的总结,除了NSTimer还有两种定时器:CADisplayLink & Dispatch Source Timer欢迎点赞和关注。

你可能感兴趣的:(NSTimer在使用中需要注意的点)