探究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创建过程

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自然就精通了。
目录

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

未完,待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,042评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,996评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,674评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,340评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,404评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,749评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,902评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,662评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,110评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,577评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,258评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,848评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,726评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,952评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,271评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,452评论 2 348

推荐阅读更多精彩内容