使用过NSTimer的应该都清楚,NSTimer会和调用对象之间循环引用,从而导致内存泄漏。下面我们通过一个小测试,来说明这个问题。我们在一个VC的viewDidLoad方法里开启一个timer,在VC的dealloc方法里停止这个timer,如果没有循环引用,那么当我们退出这个VC之后,会调用VC的dealloc方法,从而停止timer,相关代码如下:
@interface TimerVC ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor grayColor];
[self startTimer];
}
- (void)startTimer {
if (!_timer) {
_timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(pollingTimer:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
[_timer fire];
}
- (void)stopTimer {
[self.timer invalidate];
self.timer = nil;
}
- (void)pollingTimer:(NSTimer *)timer {
NSLog(@"*****************TimerVC pollingTimer");
}
- (void)dealloc {
[self stopTimer];
}
@end
运行之后发现,即便退出了TimerVC,其内部的timer仍然没有停止,一直在后台打印log,而且不会调用TimerVC的dealloc方法,这是因为timer和VC相互引用,导致谁也不会被释放。为了解决这个问题,iOS 10中新增了两个API,允许我们使用block的方式执行定时代码,这两个API如下:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
我们把上面的startTimer方法改成如下方式:
- (void)startTimer {
if (!_timer) {
_timer = [NSTimer timerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"*****************TimerVC pollingTimer");
}];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
[_timer fire];
}
重新执行,发现关闭TimerVC之后,stopTimer方法得以执行,说明使用block方式确实能解决循环引用。但使用block的方式也会有两个至关重要的问题:
1. 我们必须在调用者的dealloc方法里手动停止timer,否则timer会一直执行,并不被释放。
2. 请找一个只支持ios 10以上的公司。。。
我们的项目中使用的是另外一种方式,代码在这里:github。当我们在项目中比如一个VC里用上述代码初始化Timer时,各对象的引用关系如下:
这样,当退出VC时,LifeTracker由于没有持有者而被回收,从而走到了LiferTracker的dealloc方法里,我们在dealloc方法里调用了timer的invalidate方法,并将timer置为nil,这时,timerProxy也没有了持有者,所以也会被释放。整个过程没有内存泄漏,而且调用者不用手动调用timer的invalidate,方便快捷。