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

airmancn 7年前
   <h2>目录</h2>    <h2>引言</h2>    <p>为什么想起来要讨论NSTimer? 源自这两天工作中的遇到的一个问题:</p>    <p>专职iOS开发也一年有余了, 但是在跟踪自己写的ViewController释放时, 发现ViewController的dealloc方法死活没走到, 心里咯噔一下, 不会又内存泄漏了? :flushed:</p>    <p>一切都是很完美的节奏啊: ViewController初始化时, 创建Sub UIView, 创建数据结构, 创建NSTimer</p>    <p>然后在dealloc里, 释放NSTimer, 然后NSTimer = nil, 哪里会有什么问题?</p>    <p>不对! 移除NSTimer后dealloc就愉快滴走了起来, 难道NSTimer的用法一直都不对?</p>    <p>结果发现, 真的是不对! :flushed:</p>    <p>好吧, 故事就讲到这里, 马上开始今天的NSTimer之旅吧</p>    <h2>创建NSTimer</h2>    <p>创建NSTimer的常用方法是</p>    <pre>  <code class="language-objectivec">+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats</code></pre>    <p>创建NSTimer的不常用方法是</p>    <pre>  <code class="language-objectivec">+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats</code></pre>    <p>和</p>    <pre>  <code class="language-objectivec">- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats</code></pre>    <p>这几种方法除了创建方式不同(参数), 方法类型不同(类方法, 对象方法), 还有其他不同么?</p>    <p>当然有, 不然Apple没必要这么作, 开这么多接口, 作者(好像就是我:smile:)也没必要这么作, 写这么长软文</p>    <p>他们的区别很简单:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cf604688092624642a9211d6b0e8bc3c.png"></p>    <p>how-to-user-nstimer-01.png</p>    <p>scheduledTimerWithTimeInterval相比它的小伙伴们不仅仅是创建了NSTimer对象, 还把该对象加入到了当前的runloop中!</p>    <p>等等, runloop是什么鬼? 在此不解释runloop的原理, 但是使用NSTimer你必须要知道的是</p>    <p>NSTimer只有被加入到runloop, 才会生效, 即NSTimer才会被真正执行</p>    <p>所以说, 如果你想使用timerWithTimeInterval或initWithFireDate的话, 需要使用NSRunloop的以下方法将NSTimer加入到runloop中</p>    <pre>  <code class="language-objectivec">- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e4671fab282aac8a69c8e9e7a8002e70.png"></p>    <p>how-to-user-nstimer-02.png</p>    <h2>销毁NSTimer</h2>    <p>知道了如何创建NSTimer后, 我们来说说如何销毁NSTimer, 销毁NSTimer不是很简单的么?</p>    <p>用invalidate方法啊, 好像还有个fire方法, 实在不行直接将NSTimer对象置nil, 这样iOS系统就帮我们销毁了</p>    <p>是的, 曾经的我也是如此混沌滴这么认为着, 那么这几种方法是不是真的都可以销毁NSTimer呢?</p>    <p>invalidate与fire</p>    <p>我们来看看Apple Documentation对这两个方法的权威解释吧</p>    <ul>     <li>invalidate</li>    </ul>    <p>Stops the receiver from ever firing again and requests its removal from its run loop</p>    <p>This method is the only way to remove a timer from an NSRunLoop object</p>    <ul>     <li>fire</li>    </ul>    <p>Causes the receiver’s message to be sent to its target</p>    <p>If the timer is non-repeating, it is automatically invalidated after firing</p>    <p>理解了上面的几句话, 你就完完全全理解了invalidate和fire的区别了, 下面的示意图更直观</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6ebcc41a5db76095e4bde7bd944bd288.png"></p>    <p>how-to-user-nstimer-03.png</p>    <p>总之, 如果想要销毁NSTimer, 那么确定, 一定以及肯定要调用invalidate方法</p>    <p>invalidate与=nil</p>    <p>就像销毁其他强应用(不用我解释强引用了吧, 否则你还是别浪费时间往下看了)对象一样, 我们是否可以将NSTimer置nil, 而让iOS系统帮我们销毁NSTimer呢?</p>    <p>答案是: 当然不可以! (详见上述的结论, "总之, 巴拉巴拉...")</p>    <p>为什么不可以? 其他强引用对象都可以, 为什么NSTimer对象不可以? 你说不可以就可以? 凭什么信你?</p>    <p>好吧, 我们来看下使用NSTimer时, ARC是怎么工作的</p>    <ul>     <li>首先, 是创建NSTimer, 加入到runloop后, 除了ViewController之外iOS系统也会强引用NSTimer对象</li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/869b3da3f085894a19e6f80cda6e0c68.png"></p>    <p>how-to-user-nstimer-04.png</p>    <ul>     <li>当调用invalidate方法时, 移除runloop后, iOS系统会解除对NSTimer对象的强引用, 当ViewController销毁时, ViewController和NSTimer就都可以释放了</li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3f4943f8f60436d1674b18f5556b09ba.png"></p>    <p>how-to-user-nstimer-05.png</p>    <ul>     <li>当将NSTimer对象置nil时, 虽然解除了ViewController对NSTimer的强引用, 但是iOS系统仍然对NSTimer和ViewController存在着强引用关系</li>    </ul>    <p>神马? iOS系统对NSTimer有强引用很好理解, 对ViewController本来不就是强引用么?</p>    <p>这里所说的iOS系统对ViewController的强引用, 不是指为了实现View显示的强引用, 而是指iOS为了实现NSTimer而对ViewController进行的额外强引用 (我去, 能不能不要这么拗口, 欺负我语文不好)</p>    <p>不瞒您说, 我的语文其实也是一般般, 所以show me the code</p>    <pre>  <code class="language-objectivec">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));</code></pre>    <p>各位请注意, 创建NSTimer和销毁NSTimer后, ViewController(就是这里的self)引用计数的变化</p>    <pre>  <code class="language-objectivec">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</code></pre>    <p>如果你还是不理解, 那只能用"杀手锏"了, 美图伺候!</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f7fdfd65a167ed244445cd3c207f3607.png"></p>    <p>how-to-user-nstimer-06.png</p>    <p>关于上图, @JasonHan0991 有不同的解释, 详见评论区, 在此表示感谢!</p>    <p>结论</p>    <p>综上所述, 销毁NSTimer的正确姿势应该是</p>    <pre>  <code class="language-objectivec">[_timer invalidate]; // 真正销毁NSTimer对象的地方  _timer = nil; // 对象置nil是一种规范和习惯</code></pre>    <p>慢着, 这个结论好像不妥吧?</p>    <p>这都被你发现了! 销毁NSTimer的时机也是至关重要的!</p>    <p>如果将上述销毁NSTimer的代码放到ViewController的dealloc方法里, 你会发现dealloc还是永远不会走的</p>    <p>所以我们要将上述代码放到ViewController的其他生命周期方法里, 例如ViewWillDisappear中</p>    <p>综上所述, 销毁NSTimer的正确姿势应该是 (这句话我怎么看着这么眼熟, 是的, 这次真的结论了)</p>    <pre>  <code class="language-objectivec">- (void)viewWillDisappear:(BOOL)animated {      [super viewWillDisappear:animated];        [_timer invalidate];      _timer = nil;  }</code></pre>    <h2>NSTimer与runloop</h2>    <p>上面说到scheduledTimerWithTimeInterval方法时, 有这么一句</p>    <p>schedules it on the current run loop in the default mode</p>    <p>加到runloop这件事就不必再解释了, 而这个default mode应该如何理解呢?</p>    <p>其实我是不想谈runloop的(因为理解不深, 所以怕误导人民群众), 但是这里不得不解释下了</p>    <p>runloop会运行在不同的mode, 简单来说有以下两种mode</p>    <ul>     <li> <p>NSDefaultRunLoopMode, 默认的mode</p> </li>     <li> <p>UITrackingRunLoopMode, 当处理UI滚动操作时的mode</p> </li>    </ul>    <p>所以scheduledTimerWithTimeInterval创建的NSTimer在UI滚动时, 是不会被及时触发的, 因为此时NSTimer被加到了default mode</p>    <p>如果想要runloop运行在UITrackingRunLoopMode时, 仍然及时触发NSTimer那应该怎么办呢?</p>    <p>应该使用timerWithTimeInterval或initWithFireDate, 在创建完NSTimer后, 自己加入到指定的runloop mode</p>    <pre>  <code class="language-objectivec">[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];</code></pre>    <p>NSRunLoopCommonModes又是什么鬼? 不是说好的只有两种mode么?</p>    <p>是滴, 请注意这里的复数形式modes, 说明它不是一个mode, 它是mode的集合!</p>    <p>通常情况下NSDefaultRunLoopMode和UITrackingRunLoopMode都已经被加入到了common modes集合中, 所以不论runloop运行在哪种mode下, NSTimer都会被及时触发</p>    <p>最后, 我们来做个小测验, 来结束今天的NSTimer讨论吧</p>    <p>测验: 请问下面的NSTimer哪个更准时?</p>    <pre>  <code class="language-objectivec">// 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];</code></pre>    <p>答案, 就不贴了, 相信你肯定知道的; 另外, 关于runloop, 计划后续会有单独的文章来详细讨论之</p>    <h2> </h2>    <p> </p>    <p>来自:http://www.jianshu.com/p/330d7310339d</p>    <p> </p>