目录
引言
为什么想起来要讨论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没必要这么作, 开这么多接口, 作者(好像就是我😄)也没必要这么作, 写这么长软文
他们的区别很简单:
scheduledTimerWithTimeInterval相比它的小伙伴们不仅仅是创建了NSTimer对象, 还把该对象加入到了当前的runloop中!
等等, runloop是什么鬼? 在此不解释runloop的原理, 但是使用NSTimer你必须要知道的是
NSTimer只有被加入到runloop, 才会生效, 即NSTimer才会被真正执行
所以说, 如果你想使用timerWithTimeInterval或initWithFireDate的话, 需要使用NSRunloop的以下方法将NSTimer加入到runloop中
- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
销毁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的区别了, 下面的示意图更直观
总之, 如果想要销毁NSTimer, 那么确定, 一定以及肯定要调用invalidate方法
invalidate与=nil
就像销毁其他强应用(不用我解释强引用了吧, 否则你还是别浪费时间往下看了)对象一样, 我们是否可以将NSTimer置nil, 而让iOS系统帮我们销毁NSTimer呢?
答案是: 当然不可以! (详见上述的结论, "总之, 巴拉巴拉...")
为什么不可以? 其他强引用对象都可以, 为什么NSTimer对象不可以? 你说不可以就可以? 凭什么信你?
好吧, 我们来看下使用NSTimer时, ARC是怎么工作的
- 首先, 是创建NSTimer, 加入到runloop后, 除了ViewController之外iOS系统也会强引用NSTimer对象
- 当调用invalidate方法时, 移除runloop后, iOS系统会解除对NSTimer对象的强引用, 当ViewController销毁时, ViewController和NSTimer就都可以释放了
- 当将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
如果你还是不理解, 那只能用"杀手锏"了, 美图伺候!
关于上图, @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, 计划后续会有单独的文章来详细讨论之
附录
更多文章, 请支持我的个人博客