Leon’s Blog

Dreams don't work unless you do.

解决NSTimer内存泄露的巧妙方式

在使用NSTimer的过程中可能会遇到控制器明明销毁了,但是就是不会走控制器的dealloc销毁方法,NSTimer也没有停止运行,这就说明在使用NSTimer过程中出现了循环引用导致的内存泄露问题,在stackoverflow上看到了一种巧妙的方式解除这种循环引用,在此记录一下。

发生内存泄漏的场景

例如我们在viewController中声明了NSTimer属性,然后在viewDidLoad中去创建一个NSTimer实例赋值给myTimer,让它在dealloc方法中对myTimer进行销毁操作(我们只考虑在dealloc方法中对其销毁):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface ViewController ()
@property (nonatomic, weak) NSTimer *myTimer;
@end

- (void)dealloc {
[_myTimer invalidate];
}

- (void)viewDidLoad {
[super viewDidLoad];

self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(logTime:) userInfo:@"打印log" repeats:YES];

}

- (void)logTime:(NSTimer *)timer {
NSLog(@"%@", timer.userInfo);
}

这个viewController控制器是我从其他控制器push过来的,这时候我又pop到上一个页面,按道理说viewController应该在这个时候会走dealloc销毁方法对_myTimer进行销毁操作,然后并没有,结合上篇对NSTimer的介绍,分析原因如下:

  1. 当前线程RunLoop对NSTimer对象强引用
  2. NSTimer又对self强引用
  3. 这样就造成了控制器(self)如果想要销毁的话,NSTimer就要先调用invalidate来销毁
  4. 但是控制器(self)不销毁的话
  5. 这就产生了一个死循环,谁也释放不掉

到这里会让我们理所当然地想到用__weak来避免循环引用:

1
2
__weak typeof(self) weakSelf = self;
self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(logTime:) userInfo:@"111" repeats:YES];

结果还是没有避免循环引用的问题,stackoverflow的回答有这样一段话:

It won’t have the effect of creating an NSTimer with a weak reference. The only difference between that code and using a __strong reference is that if self is deallocated in between the two lines given then you’ll pass nil to the timer.

意思是说 weak 和 strong 唯一的区别就是:如果在这两行代码执行的期间 self 被释放了, NSTimer 的 target 会变成 nil。我们用下面这段代码来理解一下这里用__weak修饰是神马意思:

1
2
3
4
5
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

weakSelf.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(logTime:) userInfo:@"111" repeats:YES];
});

我们让NSTimer添加的代码延后2s执行,进入到这个控制器后立即(2s内)pop出控制器,然后在block中打断点打印self,这时候self是空的,NSTimer的target也就自然而然是nil。

解决办法

想解决这个问题,其实就是想办法让NSTimer不强引用self,那么我们可以创建一个中间对象来替代self,让NSTimer的target强引用这个中间对象,具体实现步骤如下:

  1. 定义一个LEOTimerTarget中间类,它的实例对象专门用来取代当前控制器(self)。
  2. 然后我们为LEOTimerTarget类定义target和selector属性,这两个属性是用来存放目标对象(这里是控制器)和要执行的目标方法(这里指控制器中的方法)。
  3. 然后在NSTimer重复执行任务的方法中判断LEOTimerTarget实例对象的target是否为空,为空的话说明目标对象(这里指当前控制器)已经销毁,接着让NSTimer实例调用invalidate销毁,如果不为空,就通过performSelector:withObject:让目标对象去执行目标方法。

代码如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@interface LEOTimerTarget : NSObject
// 目标对象
@property (nonatomic,weak) id target;
// 目标方法
@property (nonatomic,assign) SEL selector;
// NSTimer实例
@property (nonatomic, weak) NSTimer *timer;
@end

@implementation LEOTimerTarget

- (void)fire:(NSTimer *)timer {
// target为弱引用,所以当target对象销毁时,target 为nil
if (self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer.userInfo];
#pragma clang diagnostic pop
}else {
[timer invalidate];
}
}



@end

// 注意这里不是LEOTimerTarget类
@implementation LEOTimer

+ (NSTimer *)scheduleTimerWithTimerInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {

LEOTimerTarget *leoTimerTarget = [[LEOTimerTarget alloc]init];
leoTimerTarget.target = aTarget;
leoTimerTarget.selector = aSelector;
leoTimerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:leoTimerTarget
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
return leoTimerTarget.timer;
}

@end

这样,我们就避免了循环引用的问题,而且无需重复去写NSTimer的销毁方法invalidate

如果更习惯block调用方式的话,我们也可以在其基础上添加一个block调用接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ (NSTimer *)scheduleTimerWithTimerInterval:(NSTimeInterval)interval
blockHandle:(LEOTimerBlockHandle)block
userInfo:(id)userInfo
repeats:(BOOL)repeats {

// 这里的self指的是LEOTimer类,它调用的是类方法timerBlockInvoke:
return [self scheduleTimerWithTimerInterval:interval
target:self
selector:@selector(timerBlockInvoke:)
userInfo:@[[block copy],userInfo]
repeats:repeats];
}

+ (void)timerBlockInvoke:(NSArray *)userInfo {
LEOTimerBlockHandle block = userInfo[0];
id info = userInfo[1];
if (block) {
block(info);
}
}

但是这种block调用方式需要我们在获取NSTimer实例对象后,在合适的位置通过调用invalidate去销毁。

参考: