×

iOS开发 之 不要告诉我你会用NSTimer!

96
诺之林
2016.07.10 23:27* 字数 1620

目录

引言

为什么想起来要讨论NSTimer? 源自这两天工作中的遇到的一个问题:

专职iOS开发也一年有余了, 但是在跟踪自己写的ViewController释放时, 发现ViewController的dealloc方法死活没走到, 心里咯噔一下, 不会又内存泄漏了? 😳

一切都是很完美的节奏啊: ViewController初始化时, 创建Sub UIView, 创建数据结构, 创建NSTimer

然后在dealloc里, 释放NSTimer, 然后NSTimer = nil, 哪里会有什么问题?

不对! 移除NSTimer后dealloc就愉快滴走了起来, 难道NSTimer的用法一直都不对?

结果发现, 真的是不对! 😳

好吧, 故事就讲到这里, 马上开始今天的NSTimer之旅吧

创建NSTimer

创建NSTimer的常用方法是

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

创建NSTimer的不常用方法是

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

这几种方法除了创建方式不同(参数), 方法类型不同(类方法, 对象方法), 还有其他不同么?

当然有, 不然Apple没必要这么作, 开这么多接口, 作者(好像就是我😄)也没必要这么作, 写这么长软文

他们的区别很简单:

how-to-user-nstimer-01.png

scheduledTimerWithTimeInterval相比它的小伙伴们不仅仅是创建了NSTimer对象, 还把该对象加入到了当前的runloop中!

等等, runloop是什么鬼? 在此不解释runloop的原理, 但是使用NSTimer你必须要知道的是

NSTimer只有被加入到runloop, 才会生效, 即NSTimer才会被真正执行

所以说, 如果你想使用timerWithTimeInterval或initWithFireDate的话, 需要使用NSRunloop的以下方法将NSTimer加入到runloop中

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
how-to-user-nstimer-02.png

销毁NSTimer

知道了如何创建NSTimer后, 我们来说说如何销毁NSTimer, 销毁NSTimer不是很简单的么?

用invalidate方法啊, 好像还有个fire方法, 实在不行直接将NSTimer对象置nil, 这样iOS系统就帮我们销毁了

是的, 曾经的我也是如此混沌滴这么认为着, 那么这几种方法是不是真的都可以销毁NSTimer呢?

invalidate与fire

我们来看看Apple Documentation对这两个方法的权威解释吧

  • invalidate

Stops the receiver from ever firing again and requests its removal from its run loop

This method is the only way to remove a timer from an NSRunLoop object

  • fire

Causes the receiver’s message to be sent to its target

If the timer is non-repeating, it is automatically invalidated after firing

理解了上面的几句话, 你就完完全全理解了invalidate和fire的区别了, 下面的示意图更直观

how-to-user-nstimer-03.png

总之, 如果想要销毁NSTimer, 那么确定, 一定以及肯定要调用invalidate方法

invalidate与=nil

就像销毁其他强应用(不用我解释强引用了吧, 否则你还是别浪费时间往下看了)对象一样, 我们是否可以将NSTimer置nil, 而让iOS系统帮我们销毁NSTimer呢?

答案是: 当然不可以! (详见上述的结论, "总之, 巴拉巴拉...")

为什么不可以? 其他强引用对象都可以, 为什么NSTimer对象不可以? 你说不可以就可以? 凭什么信你?

好吧, 我们来看下使用NSTimer时, ARC是怎么工作的

  • 首先, 是创建NSTimer, 加入到runloop后, 除了ViewController之外iOS系统也会强引用NSTimer对象
how-to-user-nstimer-04.png
  • 当调用invalidate方法时, 移除runloop后, iOS系统会解除对NSTimer对象的强引用, 当ViewController销毁时, ViewController和NSTimer就都可以释放了
how-to-user-nstimer-05.png
  • 当将NSTimer对象置nil时, 虽然解除了ViewController对NSTimer的强引用, 但是iOS系统仍然对NSTimer和ViewController存在着强引用关系

神马? iOS系统对NSTimer有强引用很好理解, 对ViewController本来不就是强引用么?

这里所说的iOS系统对ViewController的强引用, 不是指为了实现View显示的强引用, 而是指iOS为了实现NSTimer而对ViewController进行的额外强引用 (我去, 能不能不要这么拗口, 欺负我语文不好)

不瞒您说, 我的语文其实也是一般般, 所以show me the code

NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
    
_timer = [NSTimer scheduledTimerWithTimeInterval:TimerInterval
                                          target:self
                                        selector:@selector(timerSelector:)
                                        userInfo:nil
                                         repeats:TimerRepeats];
    
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
    
[_timer invalidate];
    
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));

各位请注意, 创建NSTimer和销毁NSTimer后, ViewController(就是这里的self)引用计数的变化

2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 7
2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 8
2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 7

如果你还是不理解, 那只能用"杀手锏"了, 美图伺候!

how-to-user-nstimer-06.png

关于上图, @JasonHan0991 有不同的解释, 详见评论区, 在此表示感谢!

结论

综上所述, 销毁NSTimer的正确姿势应该是

[_timer invalidate]; // 真正销毁NSTimer对象的地方
_timer = nil; // 对象置nil是一种规范和习惯

慢着, 这个结论好像不妥吧?

这都被你发现了! 销毁NSTimer的时机也是至关重要的!

如果将上述销毁NSTimer的代码放到ViewController的dealloc方法里, 你会发现dealloc还是永远不会走的

所以我们要将上述代码放到ViewController的其他生命周期方法里, 例如ViewWillDisappear中

综上所述, 销毁NSTimer的正确姿势应该是 (这句话我怎么看着这么眼熟, 是的, 这次真的结论了)

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    [_timer invalidate];
    _timer = nil;
}

NSTimer与runloop

上面说到scheduledTimerWithTimeInterval方法时, 有这么一句

schedules it on the current run loop in the default mode

加到runloop这件事就不必再解释了, 而这个default mode应该如何理解呢?

其实我是不想谈runloop的(因为理解不深, 所以怕误导人民群众), 但是这里不得不解释下了

runloop会运行在不同的mode, 简单来说有以下两种mode

  • NSDefaultRunLoopMode, 默认的mode

  • UITrackingRunLoopMode, 当处理UI滚动操作时的mode

所以scheduledTimerWithTimeInterval创建的NSTimer在UI滚动时, 是不会被及时触发的, 因为此时NSTimer被加到了default mode

如果想要runloop运行在UITrackingRunLoopMode时, 仍然及时触发NSTimer那应该怎么办呢?

应该使用timerWithTimeInterval或initWithFireDate, 在创建完NSTimer后, 自己加入到指定的runloop mode

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

NSRunLoopCommonModes又是什么鬼? 不是说好的只有两种mode么?

是滴, 请注意这里的复数形式modes, 说明它不是一个mode, 它是mode的集合!

通常情况下NSDefaultRunLoopMode和UITrackingRunLoopMode都已经被加入到了common modes集合中, 所以不论runloop运行在哪种mode下, NSTimer都会被及时触发

最后, 我们来做个小测验, 来结束今天的NSTimer讨论吧

测验: 请问下面的NSTimer哪个更准时?

// 1
[NSTimer scheduledTimerWithTimeInterval:TimerInterval
                                 target:self
                               selector:@selector(timerSelector:)
                               userInfo:nil
                                repeats:TimerRepeats];

// 2
[[NSRunLoop currentRunLoop] addTimer:_timer
                             forMode:NSDefaultRunLoopMode];

// 3
[[NSRunLoop currentRunLoop] addTimer:_timer
                             forMode:NSRunLoopCommonModes];

答案, 就不贴了, 相信你肯定知道的; 另外, 关于runloop, 计划后续会有单独的文章来详细讨论之

附录

更多文章, 请支持我的个人博客

iOS
Web note ad 1