美文网首页iOS收藏iOS 深度好文iOS最实用干货
iOS开发 之 不要告诉我你会用NSTimer!

iOS开发 之 不要告诉我你会用NSTimer!

作者: 诺之林 | 来源:发表于2016-07-10 23:27 被阅读8952次

    目录

    引言

    为什么想起来要讨论NSTimer? 源自这两天工作中的遇到的一个问题:

    专职iOS开发也一年有余了, 但是在跟踪自己写的ViewController释放时, 发现ViewController的dealloc方法死活没走到, 心里咯噔一下, 不会又内存泄漏了? 😳

    一切都是很完美的节奏啊: ViewController初始化时, 创建Sub UIView, 创建数据结构, 创建NSTimer

    然后在dealloc里, 释放NSTimer, 然后NSTimer = nil, 哪里会有什么问题?

    不对! 移除NSTimer后dealloc就愉快滴走了起来, 难道NSTimer的用法一直都不对?

    结果发现, 真的是不对! 😳

    好吧, 故事就讲到这里, 马上开始今天的NSTimer之旅吧

    创建NSTimer

    创建NSTimer的常用方法是

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
    

    创建NSTimer的不常用方法是

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
    

    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
    

    这几种方法除了创建方式不同(参数), 方法类型不同(类方法, 对象方法), 还有其他不同么?

    当然有, 不然Apple没必要这么作, 开这么多接口, 作者(好像就是我😄)也没必要这么作, 写这么长软文

    他们的区别很简单:

    how-to-user-nstimer-01.png

    scheduledTimerWithTimeInterval相比它的小伙伴们不仅仅是创建了NSTimer对象, 还把该对象加入到了当前的runloop中!

    等等, runloop是什么鬼? 在此不解释runloop的原理, 但是使用NSTimer你必须要知道的是

    NSTimer只有被加入到runloop, 才会生效, 即NSTimer才会被真正执行

    所以说, 如果你想使用timerWithTimeInterval或initWithFireDate的话, 需要使用NSRunloop的以下方法将NSTimer加入到runloop中

    - (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
    
    how-to-user-nstimer-02.png

    销毁NSTimer

    知道了如何创建NSTimer后, 我们来说说如何销毁NSTimer, 销毁NSTimer不是很简单的么?

    用invalidate方法啊, 好像还有个fire方法, 实在不行直接将NSTimer对象置nil, 这样iOS系统就帮我们销毁了

    是的, 曾经的我也是如此混沌滴这么认为着, 那么这几种方法是不是真的都可以销毁NSTimer呢?

    invalidate与fire

    我们来看看Apple Documentation对这两个方法的权威解释吧

    • invalidate

    Stops the receiver from ever firing again and requests its removal from its run loop

    This method is the only way to remove a timer from an NSRunLoop object

    • fire

    Causes the receiver’s message to be sent to its target

    If the timer is non-repeating, it is automatically invalidated after firing

    理解了上面的几句话, 你就完完全全理解了invalidate和fire的区别了, 下面的示意图更直观

    how-to-user-nstimer-03.png

    总之, 如果想要销毁NSTimer, 那么确定, 一定以及肯定要调用invalidate方法

    invalidate与=nil

    就像销毁其他强应用(不用我解释强引用了吧, 否则你还是别浪费时间往下看了)对象一样, 我们是否可以将NSTimer置nil, 而让iOS系统帮我们销毁NSTimer呢?

    答案是: 当然不可以! (详见上述的结论, "总之, 巴拉巴拉...")

    为什么不可以? 其他强引用对象都可以, 为什么NSTimer对象不可以? 你说不可以就可以? 凭什么信你?

    好吧, 我们来看下使用NSTimer时, ARC是怎么工作的

    • 首先, 是创建NSTimer, 加入到runloop后, 除了ViewController之外iOS系统也会强引用NSTimer对象
    how-to-user-nstimer-04.png
    • 当调用invalidate方法时, 移除runloop后, iOS系统会解除对NSTimer对象的强引用, 当ViewController销毁时, ViewController和NSTimer就都可以释放了
    how-to-user-nstimer-05.png
    • 当将NSTimer对象置nil时, 虽然解除了ViewController对NSTimer的强引用, 但是iOS系统仍然对NSTimer和ViewController存在着强引用关系

    神马? iOS系统对NSTimer有强引用很好理解, 对ViewController本来不就是强引用么?

    这里所说的iOS系统对ViewController的强引用, 不是指为了实现View显示的强引用, 而是指iOS为了实现NSTimer而对ViewController进行的额外强引用 (我去, 能不能不要这么拗口, 欺负我语文不好)

    不瞒您说, 我的语文其实也是一般般, 所以show me the code

    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
        
    _timer = [NSTimer scheduledTimerWithTimeInterval:TimerInterval
                                              target:self
                                            selector:@selector(timerSelector:)
                                            userInfo:nil
                                             repeats:TimerRepeats];
        
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
        
    [_timer invalidate];
        
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
    

    各位请注意, 创建NSTimer和销毁NSTimer后, ViewController(就是这里的self)引用计数的变化

    2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 7
    2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 8
    2016-07-06 13:53:21.950 NSTimerAndDeallocDemo[2028:697020] Retain count is 7
    

    如果你还是不理解, 那只能用"杀手锏"了, 美图伺候!

    how-to-user-nstimer-06.png

    关于上图, @JasonHan0991 有不同的解释, 详见评论区, 在此表示感谢!

    结论

    综上所述, 销毁NSTimer的正确姿势应该是

    [_timer invalidate]; // 真正销毁NSTimer对象的地方
    _timer = nil; // 对象置nil是一种规范和习惯
    

    慢着, 这个结论好像不妥吧?

    这都被你发现了! 销毁NSTimer的时机也是至关重要的!

    如果将上述销毁NSTimer的代码放到ViewController的dealloc方法里, 你会发现dealloc还是永远不会走的

    所以我们要将上述代码放到ViewController的其他生命周期方法里, 例如ViewWillDisappear中

    综上所述, 销毁NSTimer的正确姿势应该是 (这句话我怎么看着这么眼熟, 是的, 这次真的结论了)

    - (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];
    
        [_timer invalidate];
        _timer = nil;
    }
    

    NSTimer与runloop

    上面说到scheduledTimerWithTimeInterval方法时, 有这么一句

    schedules it on the current run loop in the default mode

    加到runloop这件事就不必再解释了, 而这个default mode应该如何理解呢?

    其实我是不想谈runloop的(因为理解不深, 所以怕误导人民群众), 但是这里不得不解释下了

    runloop会运行在不同的mode, 简单来说有以下两种mode

    • NSDefaultRunLoopMode, 默认的mode

    • UITrackingRunLoopMode, 当处理UI滚动操作时的mode

    所以scheduledTimerWithTimeInterval创建的NSTimer在UI滚动时, 是不会被及时触发的, 因为此时NSTimer被加到了default mode

    如果想要runloop运行在UITrackingRunLoopMode时, 仍然及时触发NSTimer那应该怎么办呢?

    应该使用timerWithTimeInterval或initWithFireDate, 在创建完NSTimer后, 自己加入到指定的runloop mode

    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    

    NSRunLoopCommonModes又是什么鬼? 不是说好的只有两种mode么?

    是滴, 请注意这里的复数形式modes, 说明它不是一个mode, 它是mode的集合!

    通常情况下NSDefaultRunLoopMode和UITrackingRunLoopMode都已经被加入到了common modes集合中, 所以不论runloop运行在哪种mode下, NSTimer都会被及时触发

    最后, 我们来做个小测验, 来结束今天的NSTimer讨论吧

    测验: 请问下面的NSTimer哪个更准时?

    // 1
    [NSTimer scheduledTimerWithTimeInterval:TimerInterval
                                     target:self
                                   selector:@selector(timerSelector:)
                                   userInfo:nil
                                    repeats:TimerRepeats];
    
    // 2
    [[NSRunLoop currentRunLoop] addTimer:_timer
                                 forMode:NSDefaultRunLoopMode];
    
    // 3
    [[NSRunLoop currentRunLoop] addTimer:_timer
                                 forMode:NSRunLoopCommonModes];
    

    答案, 就不贴了, 相信你肯定知道的; 另外, 关于runloop, 计划后续会有单独的文章来详细讨论之

    附录

    更多文章, 请支持我的个人博客

    相关文章

      网友评论

      • 愿红尘作伴:我写了正计时的功能,timer释放掉了,但是下次创建的timer不是从0开始计算的,大佬这是为啥啊
      • 移动端_小刚哥:666写的挺好的
      • Liberalism:[NSTimer scheduledTimerWithTimeInterval:1.0
        repeats:YES
        block:^(NSTimer * _Nonnull timer) {

        NSLog(@"jjjj");
        }];

        请问这种情况如何处理呢?
        Eddy_0:搞个全局 timer, 在 deinit 方法里面销毁就行了
      • 超_iOS:- (void)viewWillDisappear:(BOOL)animated 在这里销毁的话,创建的地方在 viewWillAppear:吗?这样貌似会创建多个nstimer
      • 程醉不知归路:timer的回调应该是异步的把。notification,delegate,kvo呢
        石Seven:如果当前界面还有二级界面可以跳的话 你的结论就会有问题了
        程醉不知归路:还有block
      • 神采飞扬_2015:结论不是很严谨,timer进行invalidate前,最好先判断一下定时器是否有效valid。
      • 旅行的光:写的很不错,但是其实有一个问题就是
        _timer = [NSTimer scheduledTimerWithTimeInterval:TimerInterval
        target:self
        selector:@selector(timerSelector:)
        userInfo:nil
        repeats:TimerRepeats];
        这个方法,timer会强引用self,在这里会形成循环引用。
        你后面也说道销毁timer不能在dealloc中,就是因为前面timer和self产生了循环引用,因此self的dealloc不会被掉用。
        以上是我的认识。算是互相交流吧,谢谢你的分享。
        Eddy_0:@不吃鸡爪 可以在 timerSelector 方法里面销毁
        在里面添加一个判断,而且判断方法是两个变量,可以随时根据你的要求更改,
        if (num == targerNum) {
        CFRunLoopStop(CFRunLoopGetCurrent())
        self.timer?.invalidate()
        self.timer = nil
        }
        不吃鸡爪:当计时器触发后,在调用invalidated之前会一直保持对target的强引用,所以控制器无法释放也就无法调用dealloc了
      • b4067f4d3883:@诺之林 2我明白了,非常感谢你的回答。但是关于1,我用的是timerWith的方法创建的,因为在滑动状态下也要运行,只能把它添加到runloop 中的commonmode中,就是在这种情况下出现先等待间隔再执行方法的问题。
      • b4067f4d3883:你好,最近在用nstimer,有两个问题想请教一下。1、nstimer 启动后总要先等待一个间隔时间再执行方法,能不能先让他执行方法再等待间隔时间
        2、我的nstimer 放在主线程但是试了下并没有阻塞线程,这是怎么回事?
        诺之林:1: 使用NSTimer的fire方法 例如: _timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerSelector:) userInfo:nil repeats:YES]; [_timer fire]; 此时会立即执行
        2: NSTimer的timerSelector默认是会在主线程执行, 但是在主线程执行任务和阻塞主线程没有一定关系, 例如交互响应都是在主线程执行的, 只有当主线程在执行大量耗时操作或发生死锁才会被阻塞
      • ao305:写的很好!讲的很明白!
      • 白猫大厨:NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));

        _timer = [NSTimer scheduledTimerWithTimeInterval:TimerInterval
        target:self
        selector:@selector(timerSelector:)
        userInfo:nil
        repeats:TimerRepeats];

        NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));

        [_timer invalidate];

        NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));

        关于你用的这个例子和你的解释,我觉得有点不妥。你说ios系统会同时对NSTimer和viewController强调引用,所以当创建timer后,viewController的引用计数+1。但是在我看来,其实就是timer对viewController进行了强调应用,原因是因为,如果要让timer运行的时候执行viewController下面的timerSelector:,timer需要知道target,并且保存这个target,以便于在以后执行这个代码 [target performSelector:], 这里的target就是指viewController。所以,timer和viewController是相互强调引用的。 但是这样看起来,就形成了retain cycle。为了解除retain cycle,我觉得,在-(void)invalidate;这个方法下,timer之前保存的target被设置为nil,强制断开了引用环。这点和设置timer = nil是差不多的。 但是invalidate还做了另外一个动作,就是解除了runloop对timer的强调引用,使得timer成功停止。 一点点愚见,我也还在研究,希望相互学习。 :smile:
        白猫大厨:@诺之林_ :+1::+1:
        诺之林:@JasonHan0991 我已经更新了本文的正文, 让大家关注此讨论
        诺之林:@JasonHan0991 哈哈, 仔细看了你的思路, 我也认为按照你的解释可能性更大些, 多谢讨论和分享, 多多交流 :+1:
      • StephenCurry300:写的确实不错……:+1::+1::+1:
      • better_mi:写的不错,受益匪浅
      • 20ed70e8f016:有帮助
      • 鐵甲陳小寶:所以有用中间代理的方式

      本文标题:iOS开发 之 不要告诉我你会用NSTimer!

      本文链接:https://www.haomeiwen.com/subject/cogdjttx.html