探究NSTimer

探究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_第1张图片
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/

可以下载下来看和runloop相关的代码,timer自然就精通了。
探究NSTimer_第2张图片
目录

等我先自己把玩把玩,有空了继续分析代码。

未完,待续

你可能感兴趣的:(探究NSTimer)