NSTimer 内存泄露

NSTimer 引起 target 不能释放

NSTimer 是 RunLoop 的一种源,这就注定了其使用过程中与其他类的不同。

1
2
3
4
5
6
7
8
9
- (NSTimer * _Nonnull)addTimer {

return [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testOne) userInfo:nil repeats:YES];
}

- (void)viewDidLoad {
[super viewDidLoad];
[self addTimer];
}

上面这个代码是在控制器中加入了 Timer,然后执行 testOne 方法,repeat 为 YES。表现形式就是,当进入这个控制器之后,testOne 方法会在 1 秒间隔重复执行。如果没有其他 timer 有关的方法,那么当控制器 dimiss 之后,这个控制器不会被销毁,testOne 方法还是会一直被调用。

处理方式一, 调用 invalidate

出现上面的问题的原因是:

NSTimer 通过 scheduleTimerWithTimeInterval: target: selector: userInfo: repeats: 这个方法创建 timer 之后,这个 timer 会被添加到 runloop 的中,同时,runloop 会对 timer 做一次引用,而 target: 中的 self 又会被 timer 引用一次。runloop 是一直在内存中引用着 timer 的,所以 self 会被一直持有,所以就不会被释放了。

NSTimer 为我们提供了一个将 timer 从它的 runloop 中移除的方法:

1
2
3
4
5
- (void)invalidate;
Description
Stops the timer from ever firing again and requests its removal from its run loop.
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

这里指出了 invalidate 是唯一一种移除的方式 … 所以一般的处理方式是在不用到 timer 的时候让其调用 invalidate 这个方法,比如上面的问题我们如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@implementation TimerVC {
NSTimer *_timer;
}

- (NSTimer * _Nonnull)addTimer {

return [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testOne) userInfo:nil repeats:YES];
}

- (void)viewDidLoad {
[super viewDidLoad];
_timer = [self addTimer];
}


- (void)viewWillDisappear:(BOOL)animated {

[_timer invalidate];
_timer = nil;
[super viewWillDisappear:animated];

}

- (void)testOne {
NSLog(@"HEllo world");
}

- (void)dealloc {

NSLog(@"dealloc ....");
}

step:

  1. 首先在当前类中添加全局变量,在 addTimer 的时候为其赋值
  2. 在该类不在使用该 timer 的时候,[_timer invalidate] 和 _timer = nil

这样就可以避免当前类不释放的问题了。但是 … 有没有觉得很不优雅,当前控制器需要在自己离开的时候照顾这个 runloop 家的宠儿的问题,而且为了它的消亡添加了一个全局变量

处理方式二:代理对象

其实,这里的问题就出在 runloop -> timer -> self 这个链中,因为 runloop 的不死,导致了 self 一直被引用。上面的做法是强行将 runloop -> timer 的应用切断。如果有其他方案的话,我们只能去想 timer -> self 这条链了,因为 runloop 是不能死掉的。

首先,为 timer 添加一个类目:

1
2
3
@interface NSTimer (Weak)
+ (NSTimer *)weakScheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
@end

其次,是在这类目中添加一个 target 类

1
2
3
4
5
6
7
8
@interface NSTimerTarget:NSObject

@property (nonatomic, weak) id delegate;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
- (void)targetSelector;

@end

接下来看一下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

- (void)targetSelector {

if (_delegate) {
[self.delegate performSelector:_selector withObject:nil];
}else {
[_timer invalidate];
_timer = nil;
}
}

- (void)dealloc {
NSLog(@"target is dealloc...");
}
@end

@implementation NSTimer (Weak)

+ (NSTimer *)weakScheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo {

NSTimerTarget *target = [NSTimerTarget new];
target.delegate = aTarget;
target.selector = aSelector;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:ti target:target selector:@selector(targetSelector) userInfo:userInfo repeats:yesOrNo];
target.timer = timer;
return target.timer;
}
@end

在这里,通过一个中间类将外部调用 timer 的类换成了这个中间类,这样 timer 对象就不会持有调用了 NSTimer 方法的对象了,除此之外,这里 target 类还需要将外部类的 selector 方重新再 targetSelector 中调用。

targetSelector 这个方法在控制器(或者其他写了 NSTimer 启动方法的类)销毁之后其实还会继续调用,所以在内部判断移除 timer 的时机。

这样,整体上我们就不需要对外部调用 timer 的类做一些特殊的处理了。

总结:

其实,网上还有其他的解决方式,后续会继续看这个问题:

  1. 通过 GCD 实现 timer, NSWeakTimer
  2. 通过代理类 实现 timer,HWWeakTimer

第一种方式,是彻底将 timer 替换成了 gcd 中的 timer;第二种方式和目前的实现方式类似,但是是引入了新的 timer 类。

其实,这个问题应该是遇到很久了,大部分都是直接将 timer 移除的方法耦合到了使用的类中,一直没有去深究这问题,这次深究完了发现:

  1. NSRunloop 的理解加深
  2. 代码的设计上要多动脑
  3. 遇到问题多考虑集中解决方案然后再继续写代码
  4. 代码的优化方式永远不能止步

最后,代码传送带

-------------本文结束谢谢欣赏-------------
Alice wechat