NSTimer深入理解
NSTimer,即计时器,用于定时执行一些任务,一次或者多次。
系统Foundation框架提供的最常用方法如下,创建一个NSTimer,并将它放到当前runloop的default mode中。
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti // 执行任务的时间间隔 target:(id)aTarget // 执行任务的对象 selector:(SEL)aSelector // 执行任务的对象方法 userInfo:(nullable id)userInfo // 用于传一些参数 repeats:(BOOL)yesOrNo; // 是否重复执行
1、怎么保证在未来某个时间点,要执行任务时,target还有效呢?target完全有可能被释放了呀。鉴于此,NSTimer会持有target对象,直到NSTimer invalidate。不同的是,一次性NSTimer会在执行完任务之后,会自动invalidate;而重复性NSTimer,则需要手动invalidate,否则会造成内存泄漏。
2、由于NSTimer会持有target对象,如果刚好target对象就是self,而NSTimer又是self的一个实例变量,就会引发循环引用:self->NSTimer->self。
@interface TimerTest () { NSTimer *_timer; // self持有_timer } @end @implementation TimerTest - (id)init { if (self = [super init]) { _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES]; // _timer持有self } return self; } - (void)dealloc { // 因为_timer持有self,所以self的引用计数>=1,也就执行不到dealloc。 [_timer invalidate]; // 而执行不到dealloc,这句也就执行不到,_timer会一直持有self。这时候只能其他地方将_timer invalidate才行。 } - (void)test { } @end
为了解决这种循环引用问题,主要有两种方法:
1)实现一个NSTimer的category,并提供block接口。
@implementation NSTimer (Safe) + (NSTimer *)safeScheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats blk:(void(^)())blk { return [self scheduledTimerWithTimeInterval:interval target:self // target变成了NSTimer类对象,类对象由系统自动回收 selector:@selector(_doBlock:) // NSTimer会把自己传过去 userInfo:[blk copy] // 把block拷贝到堆上,避免栈block到用时已经被回收了 repeats:repeats]; } + (void)_doBlock:(NSTimer *)timer { void(^blk)() = timer.userInfo; if(blk) { blk(); } } @end @interface TimerTest () { NSTimer *_timer; // self持有_timer } @end @implementation TimerTest - (id)init { if (self = [super init]) {
__weak TimerTest *weakSelf = self; _timer = [NSTimer safeScheduledTimerWithTimeInterval:1 repeats:YES blk:^{
__strong TimerTest *strongSelf = weakSelf;
[strongSelf test];
}]; } return self; } - (void)dealloc { [_timer invalidate]; } - (void)test { } @end
3、NSTimer必须放在runloop中才能生效。如下方法就只是创建一个NSTimer,而并没有把它放到runloop中。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
需要调用如下方法把它放到runloop中,才能生效。
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
https://developer.apple.com/documentation/foundation/nstimer/2091889-scheduledtimerwithtimeinterval