[iOS]解决NSTimer造成的内存泄露

NSTimer,没错,定时器。我们开发中经常使用到的一个东西,而且我们在使用它的时候差不多都是按照以下代码来使用的:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

}

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

but,如果你将timer所属的控制器推出后,发现timer此时还在执行,这是因为timer的执行需要依赖于runloop,在timer创建好放入runloop之后,并且如果timer是循环执行的,如果不显示调用invalidate方法,那么timer是停不下来的。
同时,如果此时你重写了这个控制器的dealloc方法,并且让这个控制器pop出navigation所管理的栈时(我的这个控制器是由navigation所push出来的),你会发现dealloc方法并不会执行,这表明控制器并没有被释放,这是为什么呢,这是因为NSTimer在添加target时,会对这个target进行retain。所以就会造成上面这种情况:控制器要释放,就要释放它的所有实例变量,当释放到timer时,timer要释放他所持有的target,而此时的target是该控制器,所以造成了循环引用,从而造成了内存泄露。
要避免这种情况,我能想到的一个办法就是在设定timer的target时,将target-action保存,target改设置为另一个和timer不存在引用关系的变量,进而避免泄露。
代码如下:
首先定义一个用来保存target-action的对象

@interface PltTimerTarget : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;

@end

@implementation PltTimerTarget

- (void)pltTimerTargetAction:(NSTimer *)timer
{
    if (self.target) {
        //该方法会在RunLoop为DefaultMode时才会调用,与timer的CommonMode冲突
        //[self.target performSelector:self.selector withObject:timer afterDelay:0.0];
      
        //该方法可以正常在CommonMode中调用,但是会报警告
        //[self.target performSelector:self.selector withObject:timer];

        //最终方法
        IMP imp = [self.target methodForSelector:self.selector];
        void (*func)(id, SEL, NSTimer*) = (void *)imp;
        func(self.target, self.selector, timer);
    } else {
        [self.timer invalidate];
        self.timer = nil;
    }
}

@end

然后我们自己定义一个方法,用来设置timer(这里我定义的是默认循环的timer,这种比不循环的要常用)

+ (instancetype)pltScheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo
{
    PltTimerTarget *timerTarget = [[PltTimerTarget alloc] init];
    timerTarget.target = aTarget;
    timerTarget.selector = aSelector;
    NSTimer *timer = [NSTimer timerWithTimeInterval:ti target:timerTarget selector:@selector(pltTimerTargetAction:) userInfo:userInfo repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    timerTarget.timer = timer;
    return timerTarget.timer;
}

这样,我们就能在代码中正常使用timer了

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer pltScheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerAction:) userInfo:@"userInfo"];
    
    //这样创建的timer,target的dealloc方法不会执行,因为timer会持有target,进而造成循环引用
//    self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerAction:) userInfo:@"userInfo" repeats:YES];
    
}

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

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%@ dealloc", self);
}

此时,dealloc方法是会执行的,并且能顺利的将timer从runloop中停止,避免了内存泄露和资源浪费。
Demo地址

推荐阅读更多精彩内容