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

目录

引言

为什么想起来要讨论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, 计划后续会有单独的文章来详细讨论之

附录

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

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

推荐阅读更多精彩内容

  • 再一次面试中被问到nstimer的争取使用方法,原理,我当时就说了[_timer invalidate],time...
    iOS开发小平哥阅读 3,987评论 1 13
  • 创建NSTimer 创建NSTimer的常用方法是: + (NSTimer *)scheduledTimerWit...
    LanWor阅读 1,319评论 0 2
  • NSTimer是iOS最常用的定时器工具之一,在使用的时候常常会遇到各种各样的问题,最常见的是内存泄漏,通常我们使...
    bomo阅读 1,113评论 0 7
  • 九宫格 时间:5分钟 关键词:小毛虫 做好自己、泥土、小马、小草、美丽、小羊、小蚂蚁、小螳螂 关键词:小毛虫、美丽...
    李宇宙rourou阅读 465评论 2 2
  • 原题 代码库的版本号是从 1 到 n 的整数。某一天,有人提交了错误版本的代码,因此造成自身及之后版本的代码在单元...
    Jason_Yuan阅读 543评论 0 1