计时器要和“运行循环”(run loop)相关联,运行循环到时会触发任务。创建NSTimer时,可以将其“预先安排”在当前的运行循环中,也可以先创建好,然后由开发者来调度。无论采用哪种方式,只有把计时器放在运行循环里,它才能正常触发任务,比如用下面的方法创建计时器,并将其预先安排在当前运行循环中:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
target:(nonnull id)aTarget
selector:(nonnull SEL)aSelector
userInfo:(nullable id)userInfo
repeats:(BOOL)yesOrNo
用此方法创建的定时器,会在指定的间隔时间之后执行任务。也可以令其反复执行任务,直到开发者稍后将其手动关闭为止。target和selector参数分别表示计时器将在哪个对象上调用哪个方法。计时器会保留其目标对象,等到自身失效时才释放对象。调用invalidate方法可以令计时器失效,执行完相关任务后,一次性计时器也会失效。开发者若将计时器设为重复执行模式,则必须手动调用invalidate才能令其停止。
由于计时器会保留其目标对象,所以反复执行任务通常会导致应用程序出现"保留环"问题,比如看以下代码:
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
#import "EOCClass.h"
@implementation EOCClass {
NSTimer *_pollTimer;
}
- (id)init {
return [super init];
}
- (void)dealloc {
[_pollTimer invalidate];
}
- (void)stopPolling {
[_pollTimer invalidate];
_pollTimer = nil;
}
- (void)startPolling {
_pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
}
- (void)p_doPoll {
//do something
}
@end
试想一下,如果创建了EOCClass实例,并调用startPolling方法,由于timer的target指向的是self,则实例和timer之间互相引用形成了保留环。要打破保留环,则必须调用stopPolling方法或者令系统将其回收(由于保留环的存在,系统绝不会将其回收),但我们并不一定保证程序执行时一定能调用到。当指向EOCClass实例的最后一个外部引用移走后,该实例会继续存活,因为timer还持有这个实例,此时就造成了内存泄露,而且这种泄露还比较严重,因为timer还会反复执行定时器任务,而且这种情况无法通过代码检测直接查出来。
这个问题可以通过block解决:
#import <Foundation/Foundation.h>
@interface NSTimer (EOCBlocksSupport)
+ (NSTimer *)eoc_timerScheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
#import "NSTimer+EOCBlocksSupport.h"
@implementation NSTimer (EOCBlocksSupport)
+ (NSTimer *)eoc_timerScheduledTimerWithTimeInterval:(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();
}
}
@end
上面的代码将定时器应执行的任务封装成block,在调用计时器函数时,将block作为参数传进去,传入参数时通过copy方法将block拷贝到堆上,否则等稍后执行到block时,该block已经无效了。这时,timer现在的target对象是EOCClass类对象,类对象无需回收,所以计时器无需担心,这套方案本身不能解决问题,但提供了解决问题的方案,比如修改上面存在内存泄露的范例:
- (void)startPolling {
// _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
_pollTimer = [NSTimer eoc_timerScheduledTimerWithTimeInterval:5.0 block:^{
[self p_doPoll];
} repeats:YES];
}
但由于block依然持有了self,还是导致了保留环,但此时将block持有的self改成weak引用的便能解决此问题
- (void)startPolling {
// _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
__weak EOCClass *weakSelf = self;
_pollTimer = [NSTimer eoc_timerScheduledTimerWithTimeInterval:5.0 block:^{
[weakSelf p_doPoll];
} repeats:YES];
}