定时器:
- 需要被添加到
Runloop
,否则不会运行,当然添加的Runloop
不存在也不会运行 - 还要指定添加到的
Runloop
的哪个模式,而且还可以指定添加到Runloop
的多个模式,模式不对也是不会运行的 -
Runloop
会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。) - timer的执行时间并不准确,系统繁忙的话,还会被跳过去
-
invalidate
调用后,timer停止运行后,就一定能从Runloop
中消除吗?
定时器的一般用法
- (void)viewDidLoad {
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
self.timer = timer;
}
- (void)timerFire {
NSLog(@"timer fire");
}
上面的代码就是我们使用定时器最常用的方式,可以总结为2个步骤:创建,添加到runloop
-
scheduled
开头的方法会自动将timer添加到当前Runloop
的default mode
并马上启动 -
timer
开头的方法需要自己添加到Runloop
并手动开启
方法参数:
-
ti(interval)
:定时器触发间隔时间,单位为秒,可以是小数。如果值小于等于0.0的话,系统会默认赋值0.1毫秒 -
invocation
:这种形式用的比较少,大部分都是block和aSelector的形式 -
yesOrNo(rep)
:是否重复,如果是YES则重复触发,直到调用invalidate
方法;如果是NO,则只触发一次就自动调用invalidate
方法 -
aTarget(t)
:发送消息的目标,timer会强引用aTarget,直到调用invalidate
方法 -
aSelector(s)
:将要发送给aTarget
的消息,如果带有参数则应:- (void)timerFireMethod:(NSTimer *)timer
声明 -
userInfo(ui)
:传递的用户信息。使用的话,首先aSelector
须带有参数的声明,然后可以通过[timer userInfo]
获取,也可以为nil,那么[timer userInfo]
就为空 -
date
:触发的时间,一般情况下我们都写[NSDate date]
,这样的话定时器会立马触发一次,并且以此时间为基准。如果没有此参数的方法,则都是以当前时间为基准,第一次触发时间是当前时间加上时间间隔ti -
block
:timer触发的时候会执行这个操作,带有一个参数,无返回值
添加到runloop,参数timer是不能为空的,否则抛出异常
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode
另外,系统提供了一个- (void)fire
方法,调用它可以触发一次:
NSTimer添加到NSRunLoop
timer必须添加到Runloop
才有效,很明显要保证两件事情,一是Runloop
存在(运行),另一个才是添加。确保这两个前提后,还有Runloop
模式的问题。
一个timer可以被添加到Runloop
的多个模式,比如在主线程中runloop一般处于NSDefaultRunLoopMode
,而当滑动屏幕的时候,比如UIScrollView
或者它的子类UITableView、UICollectionView
等滑动时Runloop
处于UITrackingRunLoopMode
模式下,因此如果你想让timer在滑动的时候也能够触发,就可以分别添加到这两个模式下。或者直接用NSRunLoopCommonModes
。
但是一个timer只能添加到一个Runloop
(Runloop
与线程一一对应关系,也就是说一个timer只能添加到一个线程)。如果你非要添加到多个Runloop
,则只有一个有效
关于强引用的问题
一般我们使用NSTimer
如下图所示
- (void)viewDidLoad {
// 代码1
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
// 代码2 上文中提到有些初始化方法会自动添加Runloop 这里是主动调用 效果都一样
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 代码3
self.timer = timer;
}
- (void)timerFire {
NSLog(@"timer fire");
}
假设代码中self(viewController)
由UINavgationController
管理 并且timer与self
之间为强引用
如图所示 分析一下4根线的由来
- L1:nav push 控制器的时候会强引用,即在push的时候产生;
- L2:是在代码3的位置产生
- L3:是在代码1的位置产生,至此L2与L3已经产生了循环引用,虽然timer还没有添加到
Runloop
- L4:是在代码2的位置产生
我们常说的NSTimer
会产生循环引用 其实就是由于timer会对self进行强引用造成的。
打破循环引用
解决循环引用,首先想到的方法就是让self对timer为弱引用weak
或者time对target
如self替换为weakSelf
然而这真的有用吗?
例:
@interface TimerViewController ()
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation TimerViewController
- (void)dealloc {
[self.timer invalidate];
NSLog(@"dealloc");
}
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(count) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
}
- (void)count {
NSLog(@"count");
}
@end
从上一个vc push
进来之后, 定时器开始启动 控制台打印count, 然后pop回去, TimerViewController
并没有走dealloc
方法 控制台依然打印count。
将self改成weakSelf
效果依然一样
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:weakSelf selector:@selector(count) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
}
分析:
设置timer为weak
我们想通过self对timer的弱引用, 在self中的dealloc
方法中让timer失效来达到相互释放的目的。但是, timer内部本身对于self有一个强引用。并且timer如果不调用invalidate
方法,就会一直存在,所以就导致了self根本释放不了, 进而我们想通过在dealloc
中设置timer失效来释放timer的方法也就行不通了。
设置self为weakSelf
用__weak
修饰self为weakSelf
, weakSelf
作为一个局部变量在一般情况下,这时候就可以打破循环引用。 但是timer
还是对weakSelf
有强引用,所以weakSelf
一直都在,即还是释放不了。self和weakSelf
在这里的区别仅仅在于,如果self
释放了,weakSelf
会置空。
(别人对于weak
和strong
的解释 我觉得挺好的 多读几遍会有更多了解)
1.(
weak
与strong
)不同的是:当一个对象不再有strong
类型的指针指向它的时候,它就会被释放,即使该对象还有_weak
类型的指针指向它;
2.一旦最后一个指向该对象的strong
类型的指针离开,这个对象将被释放,如果这个时候还有weak
指针指向该对象,则会清除掉所有剩余的weak
指针
解决办法
总结: 要想解决循环引用的问题, 关键在于让timer失效即调用[timer invalidate]
方法而不是各种weak。
- 外部调用方法触发
invalidate
- (void)stopTimer {
[self.timer invalidate];
}
- 如果在是在view中的定时器, 可以重写
removeFromSuperview
- (void)removeFromSuperview {
[super removeFromSuperview];
[self.timer invalidate];
}
- 将timer的target设为一个中间类
@interface BreakTimeLoop ()
// 这里必须用weak 不然释放不了
@property (nonatomic, weak) id owner;
@end
@implementation BreakTimeLoop
- (void)dealloc {
NSLog(@"BreakTimeLoop dealloc");
}
- (instancetype)initWithOwner:(id)owner {
if (self = [super init]) {
self.owner = owner;
}
return self;
}
- (void)doSomething:(NSTimer *)timer {
// 需要参数可以通过 timer.userInfo 获取
[self.owner performSelector:@selector(count)];
}
在vc中
@interface TimerViewController ()
/** timer在这种情况下也可用strong **/
@property (nonatomic, strong) NSTimer *timer;
/** 中间类 这里用strong和weak无所谓 主要是breaker中对owner的引用为weak就行 **/
@property (nonatomic, strong) BreakTimeLoop *breaker;
@end
@implementation TimerViewController
- (void)dealloc {
[self.timer invalidate];
NSLog(@"TimerViewController dealloc");
}
- (void)viewDidLoad {
[super viewDidLoad];
self.breaker = [[BreakTimeLoop alloc] initWithOwner:self];
// 这里的doSomething: 如果在.h中没有声明会报警告 不过不影响实现
self.timer = [NSTimer timerWithTimeInterval:1.0f target:self.breaker selector:@selector(doSomething:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)count {
NSLog(@"count");
}
@end
控制台打印:
- 给
NSTimer
写一个分类
@interface NSTimer (SLBreakTimer)
+ (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block;
@end
@implementation NSTimer (SLBreakTimer)
+ (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block {
// copy将block放入堆上防止提前释放
return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(sl_block:) userInfo:[block copy] repeats:repeats];
}
+ (void)sl_block:(NSTimer *)timer {
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
在vc中
- (void)viewDidLoad {
[super viewDidLoad];
// 为weak防止循环引用
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer sl_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
// 防止self提前释放
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf count];
}];
}
控制台打印为:
- 采用block方式(iOS10之后才有)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
invalidate
方法有啥用
在上文中, 我们所有做的一些都是围绕invalidate
方法来做的。那么这个方法到底有什么用呢?
- 将timer从
Runloop
中移除 - 释放他自身持有的资源 比如
target userInfo block
注意:
- 如果timer的引用为
weak
,在调用了invalidate
之后,timer会被释放(ARC
会置空)如果在这之后还想用timer必须重新创建,所以 我们添加timer进Runloop
之前可以通过timer的isValid
方法判断是否可用. - 如果
invalidate
的调用不在添加Runloop
的线程,那么timer虽然会释放他持有的资源,但是它本身不会被释放,他所在的Runloop
也不会被释放,也会导致内存泄漏。
timer是否准时
不准时
- 第一种不准时:有可能跳过去
- 线程在处理耗时的事情时会发生。
- 还有就是timer添加到的
Runloop
模式不是Runloop
当前运行的模式,这种情况经常发生。
对于第一种情况我们不应该在timer上下功夫,而是应该避免这个耗时的工作。那么第二种情况,作为开发者这也是最应该去关注的地方,要留意,然后视情况而定是否将timer添加到Runloop
多个模式。
虽然跳过去,但是,接下来的执行不会依据被延迟的时间加上间隔时间,而是根据之前的时间来执行。比如:
定时时间间隔为2
秒,t1
秒添加成功,那么会在t2、t4、t6、t8、t10
秒注册好事件,并在这些时间触发。假设第3
秒时,执行了一个超时操作耗费了5.5
秒,则触发时间是:t2、t8.5、t10
,第4
和第6
秒就被跳过去了,虽然在t8.5
秒触发了一次,但是下一次触发时间是t10
,而不是t10.5
。
- 第二种不准时:不准点
比如上面说的t2、t4、t6、t8、t10
,并不会在准确的时间触发,而是会延迟个很小的时间,原因也可以归结为2点:
-
RunLoop
为了节省资源,并不会在非常准确的时间点触发 - 线程有耗时操作,或者其它线程有耗时操作也会影响
iOS7
以后,timer 有个属性叫做 Tolerance
(时间宽容度,默认是0),标示了当时间点到后,容许有多少最大误差。
它只会在准确的触发时间到加上Tolerance
时间内触发,而不会提前触发(是不是有点像我们的火车,只会晚点。。。)。另外可重复定时器的触发时间点不受Tolerance
影响,即类似上面说的t8.5
触发后,下一个点不会是t10.5
,而是t10 + Tolerance
。