iOS | 小心NSTimer中的内存泄漏

作者: Mrshang110 | 来源:发表于2016-07-03 14:06 被阅读6463次

NSTimer大家都很熟悉,觉得用起来也很简单。然而,由NSTimer引起的内存泄漏,不经历过一次,一般很难查。
下面看一段代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _timer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                              target:self
                                            selector:@selector(p_doSomeThing)
                                            userInfo:nil
                                             repeats:YES];

}

- (void)p_doSomeThing {
    // doSomeThing
}

- (void)p_stopDoSomeThing {
    [self.timer invalidate];
    self.timer = nil;
}

- (void)dealloc {
     [self.timer invalidate];
}

上面的代码主要是利用定时器重复执行p_doSomeThing方法,在合适的时候调用p_stopDoSomeThing方法使定时器失效。
能看出问题吗?在开始讨论上面代码问题之前,需要对NSTimer做一点说明。NSTimer的

scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:

方法的最后一个参数为YES时,NSTimer会保留目标对象,等到自身失效才释放。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用invalidate方法才会失效。

了解scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:最后一个参数含义之后,你发现何处出现问题了吗?

创建定时器时,当前控制器(创建定时器的那个控制器,为了描述方便,简称当前控制器)引用而定时器了(为什么因引用定时器?后续要用到这个定时器对象),在给定时器添加任务时,定时器保留了self(当前控制器),这里就出现了循环引用。

注:在iOS10中,新的定时器即使不被引用,也可以正常运行,控制器可以弱引用定时器,这样可以不存在循环引用,但依然会导致内存泄漏,原因是Runloop对定时源观察者要保留以便时间点到了进行调用。

循环引用

如果能在合适的时候打破循环引用,就不会有问题了。此时有两种选择:

1.控制器不再引用定时器
2.定时器不再保留当前控制器

第一种是不可行的,那么就只有第二种方法了。也就是合适的时候调用p_stopDoSomeThing方法。然而,合适的时机很难找到。假如这是一个验证码倒计时程序,你可以在倒计时结束时调用p_stopDoSomeThing方法。但是你不能确定用户一定会等倒计时结束才返回到上一级页面.或许你想在dealloc方法中使定时器失效,那你就太天真了。此时定时器还保留着当前控制器,此方法是不可能调用的,因此会出现内存泄漏。或许在倒计时程序中,你可以重写返回方法,先调用p_stopDoSomeThing再返回,但这不是一个好主意。
该问题出现的根本原因就是无法确保一定会调用p_stopDoSomeThing方法。针对这一问题,有些人会选择自己实现一个不保留目标对象的定时器。这里,并不打算采用那种从头写起的方法,正如AFN作者所说的

无数开发者尝试自己做一个简陋而脆弱的系统来实现网络缓存的功能,殊不知 NSURLCache 只要两行代码就能搞定且好上 100 倍。

这里采用block块的方法为NSTimer增加一个分类,具体细节看代码(程序员最好的语言是代码)。

//.h文件
#import <Foundation/Foundation.h>

@interface NSTimer (SGLUnRetain)
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
                                        repeats:(BOOL)repeats
                                          block:(void(^)(NSTimer *timer))block;
@end

//.m文件
#import "NSTimer+SGLUnRetain.h"

@implementation NSTimer (SGLUnRetain)

+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
    
    return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(sgl_blcokInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)sgl_blcokInvoke:(NSTimer *)timer {
    
    void (^block)(NSTimer *timer) = timer.userInfo;
    
    if (block) {
        block(timer);
    }
}
@end

//控制器.m

#import "ViewController.h"
#import "NSTimer+SGLUnRetain.h"

//定义了一个__weak的self_weak_变量
#define weakifySelf  \
__weak __typeof(&*self)weakSelf = self;

//局域定义了一个__strong的self指针指向self_weak
#define strongifySelf \
__strong __typeof(&*weakSelf)self = weakSelf;

@interface ViewController ()

@property(nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block NSInteger i = 0;
    weakifySelf
    self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
        strongifySelf
        [self p_doSomething];
        NSLog(@"----------------");
        if (i++ > 10) {
            [timer invalidate];
        }
    }];
}

- (void)p_doSomething {
    
}

- (void)dealloc {
      // 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用(具体请参阅官方文档)
     [self.timer invalidate];
}
@end

在使用中,最需要注意的就是下面这段代码:

weakifySelf
self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
       strongifySelf
       [self p_doSomething];
       NSLog(@"----------------");
       if (i++ > 10) {
           [timer invalidate];
       }
   }];

weakifySelf宏定义了一个弱引用的weakSelfstrongifySelf宏又根据weakSelf定义了一个强引用的self。这样在block内使用self和其他地方一样。这里的self和block外的self指向一样,但是不同的变量。

2017.1.6日补充:
在iOS10中,新的定时器即使不被引用,也可以正常运行,但是这依然会导致target不能释放,上面的方法依然十分有用。iOS10中,定时器的API新增了block方法,实现原理和这一样,只不过我这里用的是分类,而系统是在原始类中直接添加方法,最终的行为是一致的。

相关文章

网友评论

  • PokerFace_u:weakSelf--->self——>timer——>block——>strongSelf——>weakSelf,其中 weakSelf-->self是弱引用,这个引用过程中,依然有对象引用timer,怎么保证 用户返回上一界面时,定时器销毁?
    Mrshang110:@PokerFace_u 看一下代码:
    - (void)dealloc {
    // 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用(具体请参阅官方文档)
    [self.timer invalidate];
    }
    PokerFace_u:返回上一个界面,并且没有
    if (i++ > 10) {
    [timer invalidate];
    }
    这个的时候,定时器能销毁吗?
    Mrshang110:通过这种方式解决,你返回到上一界面,控制器就释放了,没有谁强引用timer,你再仔细看看
  • 755d07785553:你好 ,请问那我如果把 [self.timer invalidate] 写在viewDidDisappear方法中是不是就可以正常销毁了?
    Mrshang110:viewDidDisappear不止返回上一级页面会调用,在进入下一级页面等情况也会调用。另外,定时器在实际使用中有可能在各种类中,这里放到控制器中只是为了示例一下。
  • 静静的coding:楼主为啥用[block copy]而不是直接传block啊
    静静的coding:楼主你看,sgl_scheduledTimerWithTimeInterval 会创建一个timer,timer被类对象持有,除非收到invalidate不会释放,block被timer持有,不会释放,不用copy啊
  • 自负的大撸sir:想问一下 timer是runLoop强引用的, 那么为什么在控制器中声明一个timer的时候 非要用strong而不用weak呐? 如果直接使用weak 控制器就不对timer强引用了 就不会造成循环引用了啊. 还是我的理解有什么问题?
    自负的大撸sir:@Mrshang110 确实...感谢!
    Mrshang110:你可以再看看最后一段,应该就理解了,即便控制器不引用定时器,定时器的不释放也会导致控制器不会释放。
  • LTSimple:这样写是不会造成循环引用了,但是我就不明白你写的 i++ > 10 就让timer invalidate 干嘛呢。那要定时器干嘛?
    LTSimple:@Mrshang110 :joy:
    Mrshang110:@LTSimple 这只示意一下怎么用而已
  • lawrenceWeiii:请问,即使不让定时器失效,也不会有内存泄露吧?
    Mrshang110:@Lawrenceweii 定时器对象重复执行时,不主动打破,不会释放的
  • MaxWells:可以 这个在10.0中有 为了兼容这个类别还是很有意义的
    Mrshang110:@撒下的承诺 之前和苹果建议过,10.0刚更新,就发现了
  • iOS_aFei:我有一个疑问:
    为什么在[NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(sgl_blcokInvoke:) userInfo:[block copy] repeats:repeats];直接传入self的弱引用不行?

    您在类别中的解决办法是将NSTimer的target设置为了NSTimer类本身,这样解决的理论是什么?
    谢谢!
    b2c7a522e27e:weak关键字:对象释放后,指针置空;被引用时,retainCount不变。weak并没有使对象释放的功能。
    答一,weakSelf解决的是,self对timer没有强引用,相当于self和timer之间的循环引用已经打破了。但"不行"的本质不是循环引用,虽然self对timer没有强引用,但是timer对self有强引用,MainRunLoop->timer->self,如果不调用-invalidate方法,timer不会从runloop中移除,也就不会释放,进而self不会释放。
    答二,楼主解决的是self和timer之间循环引用的问题,想到用类对象,很巧妙。
    对于NSTimer避免内存泄漏(注意,不仅仅是循环引用造成的),一定要调用-invalidate方法
    对于上述理论,可以用下面的代码验证,viewController会被析构,但计时器仍会一直打印
    ```
    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"11111");
    }];
    ```
    Mrshang110:@iOS_aFei 这和block内捕获不一样,定时器对会target 进行保留,你可以理解为把它的引用计数加一;答二,这里的self是类对象,而不是实例对象
  • 小小棒棒糖:controller释放掉了,但是,这导致NSTimer无法释放
    小小棒棒糖:@Mrshang110 对于你说的这个没有合适时机问题,我非常赞同,而且你也给出了一个很好的解决方法。但你文中说是循环引用导致的不释放,经过我的验证,根本不是那样。验证方法:建立局部变量timer,然后把target指向自己随便写的一个action类,确实是timer与action都不释放,按照文中理论,此时是由于timer与target形成的循环引用。此时把action类执行CFRelease,会看到action类释放,然而,timer并没有释放,足以说明问题。等到timer下一次触发时会挂掉。代码如下:TimerAction *action = [[TimerAction alloc] init];
    [NSTimer scheduledTimerWithTimeInterval:5 target:action selector:@selector(action) userInfo:nil repeats:YES];CFRelease((__bridge CFTypeRef)action);TimerAction类里写的是定时器触发打印与dealloc打印。
    Mrshang110:@心檠 看来你还是没理解,定时器内部是不会相互引用的,请注意区分实例对象和类对象以及类对象的生命周期,等你明白这些,你就不会有这些疑问了。还有,一些场景根本没有时机去调用[timer invalidate]方法,文章详细描述了为什么没有合适时机。
    小小棒棒糖: @Mrshang110 定时器不释放的点不在controller相互引用,而是定时器的内部设计。你不把target指到别处,只要调[timer invalidate]一样能释放。如果只是相互引用的话,你weak其中一个就打破循环引用了,你可以试下真实结果。
  • Mrshang110:@vision123 你是直接复制的吗?看报错,可能你参数弄错了,错误地调了定时器的拷贝方法
    一剑寒潇:@Mrshang110 现在可以了!这么设计的思路就是巧妙的把timer的target设置为了timer的分类,从而消除了timer对vc的引用,是吧?
    Mrshang110:@vision123 已经改好了,这个+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval block:(void (^)())block repeats:(BOOL)repeats 方法里又调了自己
    一剑寒潇:@Mrshang110 报错后我又直接复制了代码,但还是报错
  • 一剑寒潇:报错!运行不起来:-[__NSCFTimer copyWithZone:]: unrecognized selector sent to instance 0x60800017f140
    25282f9e7081:你可能把selector调用的方法写成对象方法了吧

本文标题:iOS | 小心NSTimer中的内存泄漏

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