探究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.
To use a timer effectively, you should be aware of how run loops operate. See Threading Programming Guide for more information.
A timer is not a real-time mechanism. If a timer’s firing time occurs during a long run loop callout or while the run loop is in a mode that isn't monitoring the timer, the timer doesn't fire until the next time the run loop checks the timer. Therefore, the actual time at which a timer fires can be significantly later. See also Timer Tolerance.
NSTimer is toll-free bridged with its Core Foundation counterpart, CFRunLoopTimerRef. See Toll-Free Bridging for more information.
别被英文吓到,重点就两个,一:注意runloop, 二:和 CFRunLoopTimerRef 是可以相互转换的。
强引用的问题也要注意。
注意runloop 我们都有遇到过,比如:
_timer = [NSTimer timerWithTimeInterval:_interval
target:[[YYTextWeakProxy alloc] initWithTarget:self]
selector:@selector(__timeAction) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
保证在runloop切换的时候,timer不会失效。此处使用 YYTextWeakProxy 来防止循环引用问题。详见"YYKit"
和 CFRunLoopTimerRef 相互转换验证,还是上面的定时器. 我们来使用符号断点来看看。给 CFRunLoopAddTimer 函数打符号断点,定时器跑起来之后就会调用到 CFRunLoopAddTimer 函数。
调用过程如下:
Foundation`+[NSTimer(NSTimer) scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]:
接下来就会调用
CoreFoundation`CFRunLoopAddTimer:
所以,NSTimer 内部实现我们就可以扒相关CF源码来看了.
当然可以给CF 函数 CFRunLoopTimerCreate 打符号断点,看看NSTimer 创建过程,我们实验发现调用过程如下:
NSTimer 执行时间精确么?
出demo
CFAbsoluteTime beginTimer = CFAbsoluteTimeGetCurrent();
NSLog(@"BEGIN NSTIMER");
self.timer = [NSTimer timerWithTimeInterval:1
repeats:YES
block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire %f",CFAbsoluteTimeGetCurrent() - beginTimer);
}];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
执行结果如下:
2017-12-07 15:42:51.676803+0800 NSTimerTest[3360:284646] timer fire 1.001259
2017-12-07 15:42:52.676188+0800 NSTimerTest[3360:284646] timer fire 2.000647
2017-12-07 15:42:53.676830+0800 NSTimerTest[3360:284646] timer fire 3.001285
2017-12-07 15:42:54.676215+0800 NSTimerTest[3360:284646] timer fire 4.000670
2017-12-07 15:42:55.676527+0800 NSTimerTest[3360:284646] timer fire 5.000984
2017-12-07 15:42:56.676867+0800 NSTimerTest[3360:284646] timer fire 6.001319
2017-12-07 15:42:57.677029+0800 NSTimerTest[3360:284646] timer fire 7.001486
2017-12-07 15:42:58.676998+0800 NSTimerTest[3360:284646] timer fire 8.001457
2017-12-07 15:42:59.677057+0800 NSTimerTest[3360:284646] timer fire 9.001513
2017-12-07 15:43:00.677090+0800 NSTimerTest[3360:284646] timer fire 10.001549
2017-12-07 15:43:01.677131+0800 NSTimerTest[3360:284646] timer fire 11.001586
2017-12-07 15:43:02.677175+0800 NSTimerTest[3360:284646] timer fire 12.001629
正常情况下,也不是精确到 1.00000 2.00000 去执行,还是有时差的。如果做一些复杂的操作呢?
我们给block 中间加入复杂的计算量,如下:
CFAbsoluteTime beginTimer = CFAbsoluteTimeGetCurrent();
NSLog(@"BEGIN NSTIMER");
self.timer = [NSTimer timerWithTimeInterval:1
repeats:YES
block:^(NSTimer * _Nonnull timer) {
CGFloat j = 8.8888888;
for (long i = 0; i< 1000000000; i++) {
j = i*3.0345;
j -= 2.2222;
}
NSLog(@"timer fire %f",CFAbsoluteTimeGetCurrent() - beginTimer);
}];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
看看block执行的时间点。打印结果如下:
2017-12-07 15:52:47.581112+0800 NSTimerTest[3652:299598] BEGIN NSTIMER
2017-12-07 15:52:51.316325+0800 NSTimerTest[3652:299598] timer fire 3.735268
2017-12-07 15:52:54.233263+0800 NSTimerTest[3652:299598] timer fire 6.652205
2017-12-07 15:52:57.223619+0800 NSTimerTest[3652:299598] timer fire 9.642562
2017-12-07 15:53:00.208143+0800 NSTimerTest[3652:299598] timer fire 12.627085
2017-12-07 15:53:03.250902+0800 NSTimerTest[3652:299598] timer fire 15.669845
2017-12-07 15:53:06.216855+0800 NSTimerTest[3652:299598] timer fire 18.635796
2017-12-07 15:53:09.267121+0800 NSTimerTest[3652:299598] timer fire 21.686063
2017-12-07 15:53:12.253707+0800 NSTimerTest[3652:299598] timer fire 24.672646
2017-12-07 15:53:15.244591+0800 NSTimerTest[3652:299598] timer fire 27.663534
2017-12-07 15:53:18.294571+0800 NSTimerTest[3652:299598] timer fire 30.713513
执行的时间就到3s左右才会执行block.
思考,所以在做倒计时的时候,就不能使用简单的计数器来计算了,比如:
__block NSUInteger count = 10;
self.timer = [NSTimer timerWithTimeInterval:1
repeats:YES
block:^(NSTimer * _Nonnull timer) {
count -= 1;
NSLog(@"还剩%@S",@(count));
if (count == 0) {
[timer invalidate];
}
}];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
正常情况下,这样子计算是没有问题的,如果同一个线程下,同一时间有计算量比较大的任务在执行,就会有很大的误差。
正确姿势:
- (void)__timeAction {
NSTimeInterval late=[_date timeIntervalSince1970]*1;
NSDate* dat = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval now=[dat timeIntervalSince1970]*1;
NSInteger _t = answerTimerCount - (int)(now - late);
if (_t < 0) {
[self __closeTimer];
return;
}
_timeText = [NSString stringWithFormat:@"倒计时 %@S",@(_t)];
}
记得在倒计时开始之前获取一下系统的时间:
_date = [NSDate date];
如果倒计时过程中按下home 键退到后台,要想倒计时准确,也可以使用上面的方式。
NSTimer 多线程问题。
像之前的写法
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
timer 的回调会在主线程执行。就像文档上说的,主要和runloop挂钩起来,如果我们想在在他线程执行呢?
1: 使用其他线程的runloop 启动定时器。比如:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
[self.timer fire];
[[NSRunLoop currentRunLoop] run];
});
注意如果不加下面的代码,是不能正确运行的
[self.timer fire];
[[NSRunLoop currentRunLoop] run];
第一行启动定时器,第二行,线程保鲜,不然只能执行一次……
第二种方式在执行回调的时候使用其他线程:
self.timer = [NSTimer timerWithTimeInterval:1
repeats:YES
block:^(NSTimer * _Nonnull timer) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始复杂的计算 %f",CFAbsoluteTimeGetCurrent() - beginTimer);
CGFloat j = 8.8888888;
for (long i = 0; i< 1000000000; i++) {
j = i*3.0345;
j -= 2.2222;
// sleep(0.5);
}
NSLog(@"timer fire %f",CFAbsoluteTimeGetCurrent() - beginTimer);
});
}];
当然,忽略这里计算时间的问题。
CF代码是开源的,源码在此
https://opensource.apple.com/tarballs/CF/
等我先自己把玩把玩,有空了继续分析代码。
未完,待续