[crash详解与防护] NSTimer crash
前言:
NSTimer会保留其目标对象,如果不加以注意,就会持有保留环,造成内存泄露。
一、 NSTimer保留环介绍
Foundation框架中的NSTimer类,提供了在某个时间执行指定方法的功能,原型如下:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
target和selector参数表示计时器将在哪个对象上调用哪个方法。repeats参数可以指定计时器是一次性的计时器还是重复模式的计时器。一次性的计时器在执行完相关任务之后就会失效。重复模式的计时器,必须手动调用invalidate方法,才能令其停止。
重复执行的计时器很容易产生保留环而不能释放,如下所示:
// SLVTimerTestViewController.m @implementation SLVTimerTestViewController { NSTimer *_aTimer; } - (void)viewDidLoad { [super viewDidLoad]; _aTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(timer_invoke) userInfo:nil repeats:YES]; } -(void)dealloc { [_aTimer invalidate]; NSLog(@"the timer vc dealloced!"); } -(void)timer_invoke { NSLog(@"timer invoke!"); }
我们发现,当SLVTimerTestViewController不再展示的时候,并不会调用dealloc的方法,也就是SLVTimerTestViewController一直都不会释放。
原因是,NSTimer在使用scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:方法时会保留其目标对象self。而_aTimer又是self的变量,self也保留了_aTimer,这样就形成了保留环。而Timer没有主动置为失效的情况下,Timer一直会保留self,所以即使SLVTimerTestViewController已经没有其它对象使用了,也还是有Timer在持有他,一直不会释放,也一直不会调用dealloc。
二、NSTimer打破保留环的解决方案
如下代码所示,可以使用block来打破保留环:可以定义一个弱引用,令其指向self,然后使块捕获这个引用,而不直接去捕获普通的self变量。也就是说,self不会为计时器所保留。当块开始执行时,立刻生成strong引用,以保证实例在执行期间持续存活。
// SLVTimerTestViewController.m @implementation SLVTimerTestViewController { NSTimer *_aTimer; } - (void)viewDidLoad { [super viewDidLoad]; __weak SLVTimerTestViewController *weakSelf = self; _aTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 repeats:YES block:^(NSTimer* timer){ SLVTimerTestViewController *strongSelf = weakSelf; [strongSelf timer_invoke]; }]; } -(void)dealloc { [_aTimer invalidate]; NSLog(@"the timer vc dealloced!"); } -(void)timer_invoke { NSLog(@"timer invoke!"); }
这样,我们就发现当没有其它对象使用SLVTimerTestViewController时,dealloc方法就会如期执行。这是在ios10之后苹果添加的新的方法,这个block的实现。在ios10之前自己可以用分类的方法实现如下:
// NSTimer+SLVBlocksSupport.m @implementation NSTimer (SLVBlocksSupport) +(NSTimer *)slv_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats { return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(slv_blockInvoke:) userInfo:[block copy] repeats:repeats]; } -(void)slv_blockInvoke:(NSTimer *)timer { void (^block)() = timer.userInfo; if(block){ block(); } } @end