目录
引言
为什么想起来要讨论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.pngscheduledTimerWithTimeInterval相比它的小伙伴们不仅仅是创建了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对象
- 当调用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
如果你还是不理解, 那只能用"杀手锏"了, 美图伺候!
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, 计划后续会有单独的文章来详细讨论之
附录
更多文章, 请支持我的个人博客
网友评论
repeats:YES
block:^(NSTimer * _Nonnull timer) {
NSLog(@"jjjj");
}];
请问这种情况如何处理呢?
_timer = [NSTimer scheduledTimerWithTimeInterval:TimerInterval
target:self
selector:@selector(timerSelector:)
userInfo:nil
repeats:TimerRepeats];
这个方法,timer会强引用self,在这里会形成循环引用。
你后面也说道销毁timer不能在dealloc中,就是因为前面timer和self产生了循环引用,因此self的dealloc不会被掉用。
以上是我的认识。算是互相交流吧,谢谢你的分享。
在里面添加一个判断,而且判断方法是两个变量,可以随时根据你的要求更改,
if (num == targerNum) {
CFRunLoopStop(CFRunLoopGetCurrent())
self.timer?.invalidate()
self.timer = nil
}
2、我的nstimer 放在主线程但是试了下并没有阻塞线程,这是怎么回事?
2: NSTimer的timerSelector默认是会在主线程执行, 但是在主线程执行任务和阻塞主线程没有一定关系, 例如交互响应都是在主线程执行的, 只有当主线程在执行大量耗时操作或发生死锁才会被阻塞
_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));
关于你用的这个例子和你的解释,我觉得有点不妥。你说ios系统会同时对NSTimer和viewController强调引用,所以当创建timer后,viewController的引用计数+1。但是在我看来,其实就是timer对viewController进行了强调应用,原因是因为,如果要让timer运行的时候执行viewController下面的timerSelector:,timer需要知道target,并且保存这个target,以便于在以后执行这个代码 [target performSelector:], 这里的target就是指viewController。所以,timer和viewController是相互强调引用的。 但是这样看起来,就形成了retain cycle。为了解除retain cycle,我觉得,在-(void)invalidate;这个方法下,timer之前保存的target被设置为nil,强制断开了引用环。这点和设置timer = nil是差不多的。 但是invalidate还做了另外一个动作,就是解除了runloop对timer的强调引用,使得timer成功停止。 一点点愚见,我也还在研究,希望相互学习。