前言
我负责努力,其余交给运气。
对于NSTimer,大家应该很熟悉,因为NSTimer的坑比较多,所以面试中也是常问问题,工作中就更不用说了。那么今天写这篇文章的起因,就是因为自己写代码的时候习惯把黄色警告解了,所以解完自己的,就看了一下小伙伴的代码,结果发现:
这个警告呢,大意是:说好的不能为空,结果你给我传空值?其实产生这个问题的原因,也是
self.timer
和_timer
的区别,self.
的话会走set/get
方法。有兴趣的可以自己查一下。
由此,引起了思考:因为我们可以在很多地方,看别人用的时候,都会告诉你,释放的时候先invalidate
再附值nil
。可是为什么呢?然后自己查了一下,发现好多坑点,一想,算了,干脆自己总结一下吧....
一、NSTimer的循环引用
1.1 NSTimer的销毁
首先,在demo中我push进了一个控制器,再这个控制器中实现如下代码:
#import "TimerViewController.h"
@interface TimerViewController ()
@property (nonatomic, weak) NSTimer* timer;
@property (nonatomic, assign) NSInteger number;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.number = 0;
// 创建定时器
NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(showTimer:) userInfo:nil repeats:YES];
// 将定时器添加到runloop中,否则定时器不会启动
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
}
- (void)showTimer: (NSTimer*)sender {
_number ++;
NSLog(@"timer run %ld",_number);
}
- (void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
NSLog(@"viewDidDisappear run");
}
- (void)dealloc{
NSLog(@"dealloc run");
if(self.timer){
[_timer invalidate];
_timer = nil;
}
}
@end
运行结果如下:
当我pop退出当前控制器的时候,其实根本没有走
dealloc
方法,所以根本没有释放成功。(不得不深思,有多少人是这样写的?之前接手过一个项目,里面有一个NSTimer
实现的弹幕,结果就是控制器退出的时候,NSTimer
并没有释放成功,当时自己也是很懵逼...)这个问题,原因是因为强引用,官方文档中有这么一段描述:
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
大概意思是:定时器运行时会与runloop
一起。runloop
会对定时器强引用,因此在将定时器添加到runloop
之后,不必使自己对定时器强引用。(所以其实我们可以不用strong
来修饰NSTimer
)
所以问题来了:runloop
会自动强引用NSTimer
,然而这不是结束,这仅仅只是开始,除了强引用NSTimer
之外,还会对target
产生强引用( [NSTimer timerWithTimeInterval: target: selector: userInfo: repeats: ]
),一般target
我们传的都是控制器(所以后面就默认是控制器了哈),所以系统会额外对控制器产生强引用(好奇的小伙伴可以自己打印一下NSTimer
添加前后控制器的retainCount
),所以只要NSTimer
还在被强引用,那么控制器也跑不了...
1.2 invalidate
既然上面说到,NSTimer
被runloop
强引用,那么我们用的invalidate
呢?官方文档:
Stops the timer from ever firing again and requests its removal from its run loop.
个人理解是:停止定时器的运行,并从runloop中删除。
所以其实invalidate
并不是释放了NSTimer
,它只是停止了NSTimer
的运行,然后解除了runloop
对NSTimer
的强引用关系罢了,然后解除对控制器的强引用,使得控制器可以正常释放NSTimer
。
那么上面代码的运行结果就显而易见了:dealloc
要等控制器销毁的时候才会调用,而NSTimer
还没有invalidate
,那么控制器一直被强引用,所以并不会走dealloc
,结果不走dealloc
就不会invalidate
... (循环引用就出现了)
所以我们可以把invalidate
放在viewDidDisappear
中,或者pop的时候调用invalidate
,而NSTimer
置空,实际上只是有的人认为这样更保险一些,因为invalidate
毕竟不是释放对象;所以其实只要你能了解并控制好NSTimer
的释放,invalidate
后置空并不是一定要使用,只是因为大家现在已经把它用成了规范和习惯,但是不用也不代表错。
需要注意的是:
- You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
必须从添加定时器的线程中调用invalidate
,否则可能不能正常从runloop中删除对象,从而阻塞线程正常退出。- Do not subclass NSTimer.
不要将NSTimer子类化。(所以一些办法是建立一个桥接类,个人觉得比较麻烦)
二、子线程启动定时器问题
我们知道NSTimer的运行一定要有runloop的存在。我们也都知道iOS是通过runloop作为消息循环机制,主线程默认启动了runloop,可是子线程没有默认的runloop,因此,我们在子线程启动定时器是不生效的。
解决办法也简单,在子线程启动一下runloop就可以了。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(showTimer:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
});
三、runloop的mode问题
runloop有五种mode:
1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
如上,滑动ScrollView
时,runloop
会切换mode
,然而我们创建NSTimer
的时候,如果不手动添加mode
,那么默认是KCFRunLoopDefaultMode
,所以当滑动ScrollView
时切换到UITrackingRunLoopMode
,NSTimer
就失效了,要等到滑动结束,mode
切换回来的时候才会继续执行。
解决办法是手动设置mode
为kCFRunLoopCommonModes
。
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
总结
文字描述很多(语文又不好),希望耐心看下来的小伙伴会有一些收货,哈哈。