美文网首页
NSTimer的循环引用问题

NSTimer的循环引用问题

作者: Silence_xl | 来源:发表于2021-05-14 20:45 被阅读0次
    NSTimer

    scheduledTimerWithTimeInterval与timerWithTimeInterval、initWithFireDate的区别

    有三种方法来创建一个定时器

    1.使用scheduledTimerWithTimeInterval
    类方法创建计时器和进度上当前运行循环在默认模式(NSDefaultRunLoopMode)

    2.使用timerWithTimerInterval
    类方法创建计时器对象没有调度运行循环(RunLoop)
    在创建它,必须手动添加计时器运行循环,通过调用adddTimer:forMode:方法相应的NSRunLoop对象

    3.使用initWithFireDate
    在创建它,必须手动添加计时器运行循环,通过使用addTimer:forMode:方法相应的NSRunLoop对象

    - (void)execute {
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
    
    - (void)execute {
        [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
        //为什么在主线程不需要这句run,那是因为主线程有RunLoop而且已经启动
        [[NSRunLoop currentRunLoop] run];
    }
    

    这两个代码效果是一样的,scheduledTimerWithTimeInterval相当于timerWithTimeInterval的两句。

    使用过NSTimer的应该都清楚,NSTimer会和调用对象之间循环引用,从而导致内存泄漏。下面我们通过一个小测试,来说明这个问题。我们在一个VC的viewDidLoad方法里开启一个timer,在VC的dealloc方法里停止这个timer,如果没有循环引用,那么当我们退出这个VC之后,会调用VC的dealloc方法,从而停止timer,相关代码如下:

    #import "TimerVC.h"
    
    @interface TimerVC ()
    
    @property (nonatomic, strong) NSTimer *timer;
    
    @end
    
    @implementation TimerVC
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor grayColor];
        [self startTimer];
    }
    
    - (void)startTimer {
        if (!_timer) {
            _timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(pollingTimer:) userInfo:nil repeats:YES];
            [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
        }
        [_timer fire];
    }
    
    - (void)stopTimer {
        [self.timer invalidate];
        self.timer = nil;
    }
    
    - (void)pollingTimer:(NSTimer *)timer {
        NSLog(@"*****************TimerVC pollingTimer");
    }
    
    - (void)dealloc {
        [self stopTimer];
    }
    
    @end
    

    运行之后发现,即便退出了TimerVC,其内部的timer仍然没有停止,一直在后台打印log,而且不会调用TimerVC的dealloc方法,这是因为timer和VC相互引用,导致谁也不会被释放。为了解决这个问题,iOS 10中新增了两个API,允许我们使用block的方式执行定时代码,这两个API如下:

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    

    我们把上面的startTimer方法改成如下方式:

    - (void)startTimer {
        if (!_timer) {
            _timer = [NSTimer timerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
                NSLog(@"*****************TimerVC pollingTimer");
            }];
            [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
        }
        [_timer fire];
    }
    

    重新执行,发现关闭TimerVC之后,stopTimer方法得以执行,说明使用block方式确实能解决循环引用。但使用block的方式也会有两个至关重要的问题:

    1. 我们必须在调用者的dealloc方法里手动停止timer,否则timer会一直执行,并不被释放。
    2. 请找一个只支持ios 10以上的公司。。。
      我们的项目中使用的是另外一种方式,代码在这里
    @interface NSTimer (YYAdd)
    
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats;
    
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats;
    
    @end
    
    //===============================================================================
    
    @implementation NSTimer (YYAdd)
    
    + (void)_yy_ExecBlock:(NSTimer *)timer {
        if ([timer userInfo]) {
            void (^block)(NSTimer *timer) = (void (^)(NSTimer *timer))[timer userInfo];
            block(timer);
        }
    }
    
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
        return [NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];
    }
    
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
        return [NSTimer timerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];
    }
    
    @end
    
    
    当我们在项目中比如一个VC里用上述代码初始化Timer时,各对象的引用关系如下: 截屏2021-05-14 下午8.42.29.png

    这样,当退出VC时,LifeTracker由于没有持有者而被回收,从而走到了LiferTracker的dealloc方法里,我们在dealloc方法里调用了timer的invalidate方法,并将timer置为nil,这时,timerProxy也没有了持有者,所以也会被释放。整个过程没有内存泄漏,而且调用者不用手动调用timer的invalidate,方便快捷。

    最终代码

    #import "TimerVC.h"
    #import "NSTimer+YYAdd.h"
    
    @interface TimerVC ()
    
    @property (nonatomic, strong) NSTimer *timer;
    
    @end
    
    @implementation TimerVC
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor grayColor];
        [self startTimer];
    }
    
    - (void)startTimer {
        if (!_timer) {
            __weak typeof(self) weakSelf = self;
            _timer = [NSTimer scheduledTimerWithTimeInterval:2 block:^(NSTimer * _Nonnull timer) {
                [weakSelf pollingTimer];
            } repeats:YES];
            [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
        }
        [_timer fire];
    }
    
    - (void)stopTimer {
        [self.timer invalidate];
        self.timer = nil;
    }
    
    - (void)pollingTimer {
        NSLog(@"*****************TimerVC pollingTimer");
    }
    
    - (void)dealloc {
        [self stopTimer];
        NSLog(@"*****************================");
    }
    
    @end
    

    相关文章

      网友评论

          本文标题:NSTimer的循环引用问题

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