iOS小知识之NSTimer的循环引用一

952 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

1.NSTimer的循环引用

1.1 常见问题

日常开发中,经常会用到NSTimer定时器,一些不正确的写法,会导致NSTimer造成循环引用,如下代码所示

- (void)viewDidLoad { 
    [super viewDidLoad];
    
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 
}
- (void)fireHome{
    num++; 
    NSLog(@"hello word - %d",num); 
}
- (void)dealloc{ 
    [self.timer invalidate];
    self.timer = nil; 
}

上述案例,一定会产生循环引用

  • 创建NSTimer时,将self传入target,导致NSTimer持有self,而self又持有timer
  • 使用NSTimerinvalidate方法,可以解除NSTimerself的持有
  • 但是案例中,NSTimerinvalidate方法,由UIViewControllerdealloc方法执行。但是selftimer持有,只要timer有效,UIViewControllerdealloc方法就不会执行。故此双方相互等待,造成循环引用

1.2 target传入弱引用对象

NSTimertarget参数传入一个弱引用的self,能否打破对self的强持有? 代码如下:

- (void)viewDidLoad { 
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self; 
    self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES]; 
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 
}
  • 肯定是不行的,因为在timerWithTimeInterval内部,使用强引用对象接收target参数,所以外部定义为弱引用对象没有任何意义 这种方式类似于以下代码:
__weak typeof(self) weakSelf = self; 
typeof(self) strongSelf = weakSelf;

在官方文档中,对target参数进行了明确说明:

timer说明.png

  • target:定时器触发时指定的消息发送到的对象。计时器维护对该对象的强引用,直到它(计时器)失效

和Block的区别: Block将捕获到的弱引用对象,赋值给一个强引用的临时变量,当Block执行完毕,临时变量会自动销毁,解除对外部变量的持有。

2.常规解决方案

2.1 更换API

使用携带Block的方法创建NSTimer,避免target的强持有

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

2.2 在适当时机调用invalidate

根据业务需求,可以将NSTimerinvalidate方法写在viewWillDisappear方法中

- (void)viewWillAppear:(BOOL)animated{ 
    [super viewWillAppear:animated]; 
    
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 
}
- (void)viewWillDisappear:(BOOL)animated{ 
    [super viewWillDisappear:animated];

    [self.timer invalidate];
    self.timer = nil;
}

或者写在didMoveTo ParentViewController方法中

- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 
}
- (void)didMoveToParentViewController:(UIViewController *)parent{ 
    if (parent == nil) { 
        [self.timer invalidate];
        self.timer = nil;
    }
}