计时器要和“运行循环”(run loop)相关联,运行循环到时会触发任务。创建NSTimer时,可以将其“预先安排”在当前的运行循环中,也可以先创建好,然后由开发者来调度。无论采用哪种方式,只有把计时器放在运行循环里,它才能正常触发任务。
NSTimer常用的创建方法有以下两种
-
第一种创建方式
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL) repeats;
-
scheduledTimerWithTimeInterval
在主线程创建的定时器会在创建后自动将timer
添加到主线程的runloop
并启动. - 主线程的
runloopMode
为NSDefaultRunLoopMode
,但是在 ScrollView 滑动时执行的是UITrackingRunLoopMode
.NSDefaultRunLoopMode
被挂起,定时器失效,等到停止滑动才恢复. - 因此需要将
timer
分别加入UITrackingRunLoopMode
和NSDefaultRunLoopMode
中
或者直接添加到[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop mainRunLoop] addTimer:timer forMode: UITrackingRunLoopMode];
NSRunLoopCommonModes
中[[NSRunLoop mainRunLoop] addTimer:timer forMode: NSRunLoopCommonModes];
-
-
第二种创建方式
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL) repeats;
-
timerWithTimeInterval
创建的定时器不会直接启动,而需要手动添加到runloop
中;为防止出现滑动视图时定时器被挂起,可直接添加到NSRunLoopCommonModes
.
-
保留环问题
重复执行模式的计时器,很容易引入保留环
@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
@implementation EOCClass{
NSTimer *_poliTimer;
}
- (id) init{
return [super init];
}
- (void)dealloc{
[_poliTimer invalidate];
}
- (void)stopPolling{
[_poliTimer invalidate];
_poliTimer = nil;
}
- (void)startPolling{
_poliTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
}
- (void)p_doPoll{
// Poll the resource
}
如果创建了本类实例,并调用了startPolling方法。创建计时器的时候,由于目标对象是self,所以要保留此实例。然而,因为计时器是用实例变量存放的,所以实例也保留了计数器,于是就产生了保留环。
调用stopPolling方法或令系统将实例回收(会自动调用dealloc方法)可以使计时器失效,从而打破循环,但无法确保startPolling方法一定调用,而由于计时器保存着实例,实例永远不会被系统回收。当EOCClass实例的最后一个外部引用移走之后,实例仍然存活,而计时器对象也就不可能被系统回收,除了计时器外没有别的引用再指向这个实例,实例就永远丢失了,造成内存泄漏。
解决方案:采用块为计时器添加新功能,为NSTimer添加分类
@interface NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
@implementation NSTimer( EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)repeats{
return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(eoc_blockInvoke:) userInfo:[block copy] repeats:repeats];
}
+ (void)eoc_blockInvoke:(NSTimer*)timer{
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
在外部调用时范例
- (void)startPolling{
__weak EOCClass *weakSelf = self;
_poliTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{
EOCClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
} repeats:YES];
}
这段代码先定义了一个弱引用指向self,然后用块捕获这个引用,这样self就不会被计时器所保留,当块开始执行时,立刻生成strong引用,保证实例在执行器继续存活。
采用这种写法之后,如果外界指向 EOCClass 实例的最后一个引用将其释放,则该实例就可为系统所回收了。回收过程中还会调用计时器的 invalidate 方法,这样的话,计时器就不会再执行任务了。此处使用 weak 引用还能令程序更加安全,因为有时开发者可能在编写 dealloc 时忘了调用计时器的 invalidate 方法,从而导致计时器再次运行,若发生此类情况,则块里的 weakSelf 会变成 nil。
要点
NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发任务之后也会失效。
反复执行任务的计时器,很容易引入保环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。这种环状保留关系,可能是直接发生的,也可能是通过其他对象间接发生的。
可以扩充NSTimer的功能,用“块”来打破保留环。不过,除非NSTimer将来在公共接口里提供此功能,否则必须创建分类,将相关实现代码加入其中。