NSTimer
NSTimer是使用最多的一种定时器,虽然但是NSTimer还是有很多的注意事项:
不注意使用的话有循环引用的隐患。因为NSTimer会强引用target,如果target再强引用NSTimer那么就会发生循环引用。比如:
1
2
3
4
5- (void)test_normal_timer1 {
self.normalStrongTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.normalStrongTimer forMode:NSRunLoopCommonModes];
[self.normalStrongTimer fire];
}上面的代码就会发生循环引用。此时即使页面返回,但是因为循环引用,ViewController的dealloc将不会被执行,因此invalidate不能放在dealloc里,只能在其他时机比如viewDidDisappear里调用。所以一般使用weak来弱引用定时器,这样就可以在dealloc里调用invalidate。
需要在合适的地方invalid定时器,否则定时器会一直强引用target从而延长target的生命周期。
在开发中很容易忘记invalid定时器,一旦忘记invalid定时器,定时器就会延长target的生命周期,比如页面返回了但实际还没有被销毁,从而产生一些诡异问题。这也是使用NSTimer必须注意的地方。
使用时必须保证有一个活跃的runloop,并且需要指定mode。因此在子线程中使用不是很方便。
精度可能不够。
网上的一个说法:创建和撤销必须在同一个线程上,在多线程环境下使用不便。(这一条存疑,经过验证在子线程创建一个定时器,在另一个子线程invalidate并没有发现有什么问题)
iOS10开始支持block使用,同样在使用block时一定要注意循环引用。
为了从根本上避免上述问题,一个弱引用target的、能够在自身销毁时自动invalid的定时器想必是极好的。解决办法就是封装GCD定时器。
GCD定时器
基本使用:
1.创建定时器源
1 | self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, |
2.设置定时器
1 | int64_t intervalInNanoseconds = (int64_t)(self.timeInterval * NSEC_PER_SEC); |
3.设置事件回调
1 | dispatch_source_set_event_handler(self.timer, ^{ |
4.启动定时器
1 | dispatch_resume(self.timer); |
5.撤销定时器
1 | dispatch_source_cancel(timer); |
问答
1. 在子线程中使用定时器
最佳实践就是使用GCD定时器,因为GCD定时器不需要添加在runloop中,并且本身也支持指定事件回调的派发队列。
如果你非得使用NSTimer+NSThread,那么必须注意runloop的操作。runloop有下面三种开启方式:
1 | - (void)run; |
run:启动前如果没有添加任何源那么runloop一启动就会马上退出,方法也会马上返回(三种方式都遵守)。而如果有添加源则runloop将一直运行,不会退出。即使手动移除了源,也不会使得runloop会退出。
runUntilDate:和run差不多,只是多了一个截止日期。截止日期到了,则退出runloop,方法返回。
runMode:运行一次runloop后会退出。比如runloop期间处理了一个输入源事件,处理后会退出runloop,方法也会返回。注意处理定时器事件不会使得runloop退出,但如果调用invalidate则会退出。所以如果你想让runloop能够退出的话最好使用该方法。
前面两种方法即使调用 [self.timer invalidate];
,runloop也不会退出(少量情况下也会退出),这样线程就会一直存在,
1 | //线程也是会强引用target的,所以这里线程和self循环引用了。 |
线程一直存在就会一直持有self,导致页面即使返回了也不会销毁。而如果在当前线程中强行调用 + (void)exit;
退出线程,会因为一些资源没有正常释放而造成内存泄露,经过验证self依然是没有被释放的。所以只能使用第三种启动方式。
因此,在子线程中使用定时器,最佳实践就是使用GCD定时器。
2.为什么MSWeakTimer的invalidate方法里必须是异步取消,同步取消不行吗?
1 | - (void)invalidate |
正如注释写的,如果我们在回调的方法里调用invalidate方法,那么将导致队列死锁。所以这里必须采用异步invalidate。
参考
NSRunLoop的退出方式 挺不错的