iOS - 计时器及冲突问题(NSTimer,dispatch_
说到计时器大家也都很清楚NSTimer计时器,GCD 计时器。下面我们来说一下这两个计时器,以及计时器在遇到UIScrollView,UITableView 滚动时的情况。
- NSTimer
- dispatch_source_create
一、NSTimer
NSTimer *time = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"aaaaaa");
}];
注意这里不用开启计时器,就可以自动开始。内部自动加到 mainloop 里面,但是如果在有滚动视图的界面中,视图滚动的时候,计时器停止,滚动停止后,计时器加速。可以用下面的方法解决这个问题
NSTimer *time = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"aaaaaa");
}];
[[NSRunLoop currentRunLoop] addTimer:time forMode:NSRunLoopCommonModes];
NSTimer *ti = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"bbbbbb");
}];
[ti fire];
注意这里计时器要手动开启(fire)否则不会执行。这里还有一个问题,就是内部没有加入到 runloop 中。只会被执行一次。解决这个问题可以把计时器加入到 runloop 中。
NSTimer *ti = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"bbbbbb");
}];
[ti fire];
[[NSRunLoop mainRunLoop] addTimer:ti forMode:NSRunLoopCommonModes];
总结:1、timerWithTimeInterval这两个类方法创建出来的对象如果不用 addTimer: forMode方法手动加入主循环池中,将不会循环执行。并且如果不手动调用fair,则定时器不会启动。
2、scheduledTimerWithTimeInterval这两个方法不需要手动调用fair,会自动执行,并且自动加入主循环池。
3、init方法需要手动加入循环池,它会在设定的启动时间启动。
4、注意在不用的时候需要把计时器销毁(invalidate)否则会造成内存问题。由于NSTimer 被 Runloop 强引用了,如果要释放就要调用 invalidate 方法。([timer invalidate]是唯一的方法将定时器从循环池中移除),如果dealloc 里调用 invalidate 方法,但是 self 又被 NSTimer 强引用了。所以要调启 self 的dealloc方法,那么就必须先销毁NSTimer。思路可以参考下面的方法
-(void)btn{
if (timer.isValid) {
[timer invalidate];
}
timer=nil;
[self dismissViewControllerAnimated:YES completion:nil];
}
二、GCD 计时器
1、验证码倒计时
__block int timeout=59;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0*NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
if (timeout <= 0)
{
dispatch_source_cancel(_timer);
dispatch_async(dispatch_get_main_queue(), ^{
[self.getCheckCodeBtn setTitle:@"重新获取" forState:UIControlStateNormal];
self.getCheckCodeBtn.userInteractionEnabled = YES;
});
}
else
{
int seconds = timeout % 60;
NSString * strTime = [NSString stringWithFormat:@"%.2d",seconds];
dispatch_async(dispatch_get_main_queue(), ^{
[self.getCheckCodeBtn setTitle:[NSString stringWithFormat:@"%@%@",strTime,@"s重新获取" ]forState:UIControlStateNormal];
self.getCheckCodeBtn.titleLabel.font = [UIFont systemFontOfSize:12];
self.getCheckCodeBtn.userInteractionEnabled = NO;
});
timeout--;
}
});
dispatch_resume(_timer);
2、动画执行,注意timer设置为全局的,否则会被立即释放,不会执行到 block 里面。下面是一个不停旋转的动画
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.001 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
while (true) {
dispatch_sync(dispatch_get_main_queue(), ^{
self.animationView.transform = CGAffineTransformRotate(self.animationView.transform, 0.001);
});
}
});
dispatch_resume(timer);
注意上面的旋转动画如果在有滑动界面的时候会出现滑动界面不可以滑动的情况。可以用下面的方法类解决UIScrollView 滑动和动画执行时的冲突
NSTimer *tim = [NSTimer timerWithTimeInterval:0.001 repeats:YES block:^(NSTimer * _Nonnull timer) {
self.animationView.transform = CGAffineTransformRotate(self.animationView.transform, 0.001);
}];
[[NSRunLoop currentRunLoop] addTimer:tim forMode:NSRunLoopCommonModes];
**注意:停止 Dispatch Timer 有两种方法,一种是使用 dispatch_suspend ,另外一种是使用 dispatch_source_cancel **
- dispatch_suspend 严格上只是把 Timer 暂时挂起,dispatch_source_cancel 则是真正意义上的取消 Timer。
- 关于取消 Timer,另外一个 很重要 的注意事项, dispatch_suspend 之后的 Timer,是不能被释放的!下面的代码会引起崩溃
dispatch_suspend(_timer);
_timer = nil; // EXC_BAD_INSTRUCTION 崩溃
因此使用 dispatch_suspend 时,Timer 本身的实例需要一直保持。使用 dispatch_source_cancel 则没有这个限制
dispatch_source_cancel(_timer);
_timer = nil; // OK
参数:dispatch_source_set_timer 中第二个参数,当我们使用 dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时;第三个参数:interval:计时器间隔多次时间执行;第四个参数:leeway:容许的时间误差。
**总结:NSTimer受runloop的影响,由于runloop需要处理很多任务,导致NSTimer的精度降低,在日常开发中,如果我们需要对定时器的精度要求很高的话,可以考虑dispatch_source_t去实现 **