–
# 目录## 九、NSTimer > < [返回目录](#目录)
1
2
3
4
5
6
7
8 1、NSTimer和NSRunLoop的关系?
2、NSTimer使用细节
3、NSTimer的创建
4、NSTimer的循环引用
5、NSTimer使用的优化
6、NSTimer的销毁问题
(1)、子线程中NSTimer的创建和销毁问题
1、NSTimer的创建
NSTimer的创建通常有两种方式,尽管都是类方法,一种是timerWithXXX,另一种scheduedTimerWithXXX。
二者最大的区别就是后者除了创建一个定时器外会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中,不添加到RunLoop中的NSTimer是无法正常工作的。
1 | @interface ViewController1 () |
2、NSTimer的修饰符
runloop强制持有timer(runloop->timer),timer会强制持有其target,未处理的情况下一般都是self(timer->self),导致self无法释放。
虽然设置timer为weak属性时候,self未强制持有timer,没构成循环应用,但还是导致了self无法释放的问题,dealloc无法执行。
2、NSTimer的循环引用
关于循环引用,我们先看一个例子:
1 | @interface ViewController1 () |
假设代码中的视图控制器由UINavigationController管理,由于self.timer是strong类型,则强引用可以表示如下:
由于,很容易看出来,由于timer本身在创建时候已经与self发生了强引用(不管target使用weak还是strong修饰,timer都会对target强引用。)。而赋值时候又由于timer是被设为strong的,而导致self与timer之间也发生了强引用,最终这两个强引用,就形成了循环引用。
所以,
①、首先,我们先解决循环引用,为了解决timer的循环应用问题,我们上面的timer属性应该使用weak。设置成weak后,L2就消失了。
②、但是即使使用了弱引用,上面的代码中ViewController1在pop退出的时候也无法正常释放,原因是在创建NSTimer时指定了target为self,这样一来造成了timer对ViewController1有一个强引用。从而导致,timer没释放的时候,viewController也是不会被释放的。为了让timer能够释放,我们就需要调用NSTimer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。所以,假设在viewController pop回去前,我们通过一个按钮来让timer调用invalidate方法,那么viewController在pop回去时候,就能够被释放了。
问题是我们一般不会有这个按钮操作,那么这时候让timer调用invalidate方法的操作,应该写在哪里呢。这时候,你可能会想到那就写在viewDidDisappear中呗。但是一旦在viewWillDisappear中写[timer invalidate]; timer = nil;那么你也得把timer的创建放在viewWillAppear中。因为我们可能执行的是push跳到下一页,再返回来的操作。然而显然将timer的创建放在viewWillAppear中这样的方式,显然会是导致当timer可能需要频繁添加。所以,我们放弃此方法,还是把timer的创建放在viewDidLoad中,然后考虑其他方法。
所以,我们最后为了让ViewController1在pop退出的时候不会因为timer的强引用,而导致无法正常释放。我们选择转移timer中的target。这样就能确保,viewController在pop退出的时候能够正常释放,从而调用viewController的dealloc方法。
附:转移timer中的target的方法通常有两种:
一种是将target分离出来独立成一个对象(在这个对象中创建NSTimer并将对象本身作为NSTimer的target),控制器通过这个对象间接使用NSTimer;
另一种方式的思路仍然是转移target,只是可以直接增加NSTimer扩展(分类),让NSTimer自身做为target,同时可以将操作selector封装到block中。
图中参考NSTimer+Block
后者相对优雅,也是目前使用较多的方案。显然Apple也认识到了这个问题,如果你可以确保代码只在iOS 10下运行就可以使用iOS 10新增的系统级block方案(上面的代码中已经贴出这种方法)。
③、如果不做②中的转移timer的target的话,那么viewController就会无法释放,造成内存泄露。
但是,我们发现通过转移timer的target后,虽然解决了UIViewController1因为被timer强引用而导致的在pop回来的时候无法释放的问题。我们的计时器,却在UIViewController1 pop退出被释放后,两个定时器仍然在运行,也就是它还没被释放。所以,我们还需要解决timer的释放。那怎么让timer释放呢?答:如果要让timer释放掉的话,需要调用NSTimer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。
invalidate方法有2个功能:一是将timer从runloop中移除,那么图中的L4就消失,二是timer本身也会释放它持有资源,比如它的target、userinfo、block等,因为这里的target是self,所以强引用L3也就消失。
所以,最终当viewController在pop退出的时候,其正常释放并调用了dealloc放。我们只需要在dealloc方法中,添加上timer调用invalidate的方法,即可以解决viewController被释放了,但timer没被释放的问题。
1 | - (void)dealloc { |
所以,一个完整的timer过程,代码如下:
1 | 略 |
3、NSTimer和NSRunLoop的关系?
只要出现NSTimer必须要有NSRunLoop,NSTimer必须依赖NSRunLoop才能执行 。NSTimer其实也是一种资源,如果看过多线程编程指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会生效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。
NSRunLoop与timer有关方法为:
1 | - (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode; //在run loop上注册timer |
注意事项:
我们通常在主线程中使用NSTimer,有个实际遇到的问题需要注意。当滑动界面时,系统为了更好地处理UI事件和滚动显示,主线程runloop会暂时停止处理一些其它事件,这时主线程中运行的NSTimer就会被暂停。解决办法就是改变NSTimer运行的mode(mode可以看成事件类型),不使用缺省的NSDefaultRunLoopMode,而是改用NSRunLoopCommonModes,这样主线程就会继续处理NSTimer事件了。具体代码如下:
1 | NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES]; |
4、NSTimer使用细节:
NSTimer上的定时任务是在创建NSTimer的线程上执行的。NSTimer的销毁和创建必须在同一个线程上操作
NSTimer要被添加到当前线程的 Runloop 里面且 Runloop 被启动,定时任务(selector或者invocation)才会触发。
以下内容摘自:NSTimer定时器进阶——详细介绍,循环引用分析与解决
- 它需要被添加到runloop,否则不会运行,当然添加的runloop不存在也不会运行;
- 还要指定添加到的runloop的哪个模式,而且还可以指定添加到runloop的多个模式,模式不对也是不会运行的
- runloop会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。)
- timer的执行时间并不准确,系统繁忙的话,还会被跳过去。(具体的两种不准时,请查看原文)
- invalidate调用后,timer停止运行后,就一定能从runloop中消除吗,资源????invalidate方法的调用必须在timer添加到的runloop所在的线程,如果不在的话:由于调用invalidate 方法后,timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了–>L1无效–>self引用计数为0,self释放–>L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。
NSTimer的强引用问题举例:
5、NSTimer使用的优化
问:为什么要在非主线程创建NSTimer?
- 将 timer 添加到主线程的Runloop里面本身会增加线程负荷;
- 如果主线程因为某些原因阻塞卡顿了,timer 定时任务触发的时间精度肯定也会受到影响;
- 有些定时任务不是UI相关的,本来就没必要在主线程执行,给主线程增加不必要的负担。当然也可以在定时任务执行时,手动将任务指派到非主线程上,但这也是有额外开销的。
6、NSTimer的销毁问题
前面我们已经简单讲过要让NSTimer销毁释放的时候,只能通过调用其invalidate来达到销毁目的。关于invalidate的第一个作用以及它在哪个线程调用的问题,我的理解如下:
invalidate方法的第一个作用将timer从runloop中移除。这里的runLoop指的应该是当前的runLoop,而不是timer被添加到的runLoop,这个纯属个人理解,未验证,因为如果是其添加的runLoop的话,那子线程timer的销毁,就不会有人说还要和timer所在的线程一致了。所以,这里的个人理解有如下:
为了销毁timer和去除runloop与timer之间的强引用,我们调用了timer的invalidate方法。
1、对于invalidate方法的调用是写在必须在timer所添加到的runloop所在的线程(如主线程)的时候,invalidate方法会将timer从runloop中移除,并且释放它持有资源。即上面的L4和L3都消失。
2、对于invalidate方法的调用不是写在必须在timer所添加到的runloop所在的线程的时候(如子线程中添加timer,在主线程中调用该timer的invalidate),虽然timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了–>L1无效–>self引用计数为0,self释放–>L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。
如果invalidate方法的调用的位置不更改的话,这时候要让L4消失的方法,
方法①手动销毁runloop。比如
1
2
3 //[[NSRunLoop currentRunLoop] run]; //将原本的方法注释掉
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:5.f];
[[NSRunLoop currentRunLoop] runUntilDate:date]; //让runloop在5s后销毁这种方式,只适用于销毁时间确定的情况。那如果销毁时间不确定怎么办?
方法②:因为当某个线程销毁时,其runloop也随之销毁,所以方法二即为通过销毁timer所在的线程,来达到销毁runloop的目的。如果是在主线程,线程一直存在,我们没法让主线程销毁。
所以,下面我们讨论的是在子线程中添加timer的时候,如果该timer的invalidate方法的调用位置不是写在对应子线程,而是写在主线程的时候,我们该怎么通过销毁子线程,来销毁runLoop。从而接触该runloop对timerd的强引用?
6.1子线程中NSTimer的创建和销毁问题
我们按上诉2中②的讨论描述的:子线程中添加timer的时候,如果该timer的invalidate方法的调用位置不是写在对应子线程,而是写在主线程的时候,写出的对应代码如下:
1 | @property (nonatomic, weak) NSTimer *threadTimer; //子线程timer |
这时候,我们该怎么通过销毁子线程,来销毁runLoop。从而接触该runloop对timerd的强引用?即怎么销毁线程?
乍看当在子线程开启runloop后,timer会一直在子线程中运行,所以子线程不会销毁,runloop也就无法停止,runloop也就没法销毁,runloop与timer之间的强引用则还是被保留着,这似乎又是个死循环。但实际上,由于上述代码runloop的mode item只有Timer,所以只要销毁timer,runloop就会退出。所以,上述的代码是没问题的,不存在内存泄露问题。
附:NSTimer上的定时任务是在创建NSTimer的线程上执行的。
–
附:以上NSTimer的内容,有空的话还可参考NSTimer,NSRunLoop,autoreleasepool,多线程的爱恨情仇,它那边讲的,和这边自己理解的基本是一样的。只是对于有些点的介绍详细不一定一样而已。
其他有空可看iOS 中的 NSTimer
## 常见笔试/面试题 [< 返回目录](#目录) ## END [< 返回目录](#目录)

