首页 > 代码库 > [crash详解与防护] NSTimer crash

[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

 

[crash详解与防护] NSTimer crash