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

作者: PlutoMa | 来源:发表于2016-05-04 22:49 被阅读1771次

    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地址

    相关文章

      网友评论

      • Chosen_Leung:可以在viewWillDisappear里面invalidate
        charlotte2018:如果是我刚进去,立马出来,viewWillDisappear是不会走的
      • qBryant:不错,学习了

      本文标题:[iOS]解决NSTimer造成的内存泄露

      本文链接:https://www.haomeiwen.com/subject/arfdrttx.html