再一次面试中被问到nstimer的争取使用方法,原理,我当时就说了[_timer invalidate],timer =nil;
但是原理没说明,后来经过查找资料查到了,引用原著作者http://www.jianshu.com/p/330d7310339d
发现ViewController的dealloc方法死活没走到, 心里咯噔一下, 不会又内存泄漏了? 😳
一切都是很完美的节奏啊: ViewController初始化时, 创建Sub UIView, 创建数据结构, 创建NSTimer
然后在dealloc里, 释放NSTimer, 然后NSTimer = nil, 哪里会有什么问题?
不对! 移除NSTimer后dealloc就愉快滴走了起来, 难道NSTimer的用法一直都不对?
结果发现, 真的是不对! 😳
好吧, 故事就讲到这里, 马上开始今天的NSTimer之旅吧
创建NSTimer
创建NSTimer的常用方法是
+ (NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)titarget:(id)targetselector:(SEL)aSelectoruserInfo:(id)userInforepeats:(BOOL)repeats
创建NSTimer的不常用方法是
+ (NSTimer*)timerWithTimeInterval:(NSTimeInterval)titarget:(id)targetselector:(SEL)aSelectoruserInfo:(id)userInforepeats:(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((__bridgeCFTypeRef)self));_timer = [NSTimerscheduledTimerWithTimeInterval:TimerInterval target:selfselector:@selector(timerSelector:) userInfo:nilrepeats:TimerRepeats];NSLog(@"Retain count is %ld",CFGetRetainCount((__bridgeCFTypeRef)self));[_timer invalidate];NSLog(@"Retain count is %ld",CFGetRetainCount((__bridgeCFTypeRef)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的正确姿势应该是
[_timerinvalidate];// 真正销毁NSTimer对象的地方_timer=nil;// 对象置nil是一种规范和习惯
慢着, 这个结论好像不妥吧?
这都被你发现了! 销毁NSTimer的时机也是至关重要的!
如果将上述销毁NSTimer的代码放到ViewController的dealloc方法里, 你会发现dealloc还是永远不会走的
所以我们要将上述代码放到ViewController的其他生命周期方法里, 例如ViewWillDisappear中
综上所述, 销毁NSTimer的正确姿势应该是 (这句话我怎么看着这么眼熟, 是的, 这次真的结论了)
- (void)viewWillDisappear:(BOOL)animated { [superviewWillDisappear: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:_timerforMode:NSRunLoopCommonModes];
NSRunLoopCommonModes又是什么鬼? 不是说好的只有两种mode么?
是滴, 请注意这里的复数形式modes, 说明它不是一个mode, 它是mode的集合!
通常情况下NSDefaultRunLoopMode和UITrackingRunLoopMode都已经被加入到了common modes集合中, 所以不论runloop运行在哪种mode下, NSTimer都会被及时触发
最后, 我们来做个小测验, 来结束今天的NSTimer讨论吧
测验: 请问下面的NSTimer哪个更准时?
// 1[NSTimerscheduledTimerWithTimeInterval:TimerInterval target:selfselector:@selector(timerSelector:) userInfo:nilrepeats:TimerRepeats];
// 2[[NSRunLoopcurrentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
// 3[[NSRunLoopcurrentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
答案, 第三种是最准确的,不受其他情况影响,第一种和第二种情况相同都是加入到NSDefaultRunLoopMode模式下,如果滑动scrollview的时候,时间计时器会停止,等滑动结束,再继续,所以时间不准确
最后,如果本文章对你有帮助,希望点赞,评论666
本人github开源库地址:https://github.com/lishiping,希望对广大iOS开发者有帮助
Cocoapod公开库:
1.SafeData---使用array,dictionary类别,主要是保护数组防止插入空数组,防止数组越界,在开发中非常实用,字典的类别也非常实用,可以直接得到转换后的类型SafeData
2.SPDebugBar ---A tool to help developers and testers quickly switch the server address, convenient to debug the program.一个小工具帮助开发人员和测试人员快速切换服务器地址,方便调试程序,可以在debug模式下或者测试包上方便切换地址SPDebugBar
3.SPFastPush---Use Macro Fast NavigationController push next VC,and Fast Pop anther VC.使用宏帮助导航控制器快速push下一个Viewcontroller,并使用KVC快速给下一个VC赋值,也可以使用宏定义快速返回上一层VC或者指定的VCSPFastPush
4.SPMacro---一些foundation层的一些宏定义,打印宏定义,类型判断,通知使用,线程使用等,和一些UIKit层的宏定义,屏幕方面的宏定义,颜色方面的宏定义,图片宏定义SPMacro
5.SPWebView---SPWebView是一个基于OC代码实现的WebView轻量级组件,将UIWebView和WKWebView的API封装成统一的类去使用,并且在加载网页的时候提供进度条,同时简化JS与OC互相调用及传递数据的方式。仿微信界面SPWebView
6.SPCategory---SPWebView是一个常用类别库,size,字符串,hud等SPCategory