小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
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 - 使用
NSTimer的invalidate方法,可以解除NSTimer对self的持有 - 但是案例中,
NSTimer的invalidate方法,由UIViewController的dealloc方法执行。但是self被timer持有,只要timer有效,UIViewController的dealloc方法就不会执行。故此双方相互等待,造成循环引用
1.2 target传入弱引用对象
对NSTimer的target参数传入一个弱引用的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参数进行了明确说明:
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
根据业务需求,可以将NSTimer的invalidate方法写在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;
}
}