iOS NSTimer循环引用问题

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」。

NSTimer是iOS中常用的定时器,通常用来在固定时间间隔重复某个任务。使用起来也比较简单,但是一直以来存在一个问题,就是会造成循环引用,下面先来看下导致循环引用的用法。

假设现在有一个控制器。

@interface MyViewController ()
@property (nonatomic, strong) NSTimer * timer;
@end
复制代码

在控制器的viewDidLoad中,我们初始化一个按钮。

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
    [btn setTitle:@"begin" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    btn.frame = CGRectMake(20, 100, 120, 80);
    [btn addTarget:self action:@selector(update) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
}
复制代码

点击按钮,初始化timer

- (void)update {
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(logNumber) userInfo:nil repeats:YES];
}
复制代码

每隔2秒执行以下方法。

- (void)logNumber {
    NSLog(@"22222");
}
复制代码

看起来很简单,代码也可以顺利运行,但是问题来了,timer作为控制的属性,初始化时,控制器强引用timer,在timer的初始化方法中,self(即控制器)又作为targettimer强引用,毫无疑问,一个循环引用出现了,这将导致两者都不能被释放,也就是说,控制器在被推出的时候,并不会执行dealloc方法,因此,下面的方法也就不会执行。timer自然也不会被释放。

- (void)dealloc {     
    [self.timer invalidate];
    self.timer = nil;
}
复制代码

那要怎么解决呢?这里介绍一种方法,引入中间者。
MyTimerMiddle是一个中间类,它主要负责timer的初始化,定时任务的触发以及timer的销毁,同时,也负责连接起最终要执行任务的目标对象。

@interface MyTimerMiddle : NSObject
// target即是最终要连接的目标对象,注意:这里使用weak,原因后面会说明。
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;

- (instancetype)initWithTimeInterval:(NSTimeInterval)interval
                              target:(id)target
                            selector:(SEL)selector;
// 销毁timer
- (void)cleanup;
@end
复制代码

timer作为它的一个属性。

#import "MyTimerMiddle.h"

@interface MyTimerMiddle ()
@property (nonatomic, strong) NSTimer * timer;
@end
复制代码

对外提供一个自定义的初始化方法

- (instancetype)initWithTimeInterval:(NSTimeInterval)interval
                              target:(id)target
                            selector:(SEL)selector {
    if (self = [super init]) {
        self.target = target;
        self.selector = selector;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(fetchData:) userInfo:nil repeats:YES];
    }
    return self;
}
复制代码

定时执行的任务,这里模拟了一个下载任务,3秒后回到主线程执行,这里是真正连接目标对象的地方。

- (void)fetchData:(NSTimer *)timer {
    NSLog(@"开始下载.....");
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(self) sself = weakSelf;
        if (!sself) {
            return;
        }
        if (sself.target == nil) {
            return;
        }
        id targrt = sself.target;
        SEL selector = sself.selector;
        if ([targrt respondsToSelector:selector]) {
            [targrt performSelector:selector withObject:@{@"name": @"mike"}];
        }
    });
}
复制代码

接下来是timer的销毁方法。

- (void)cleanup {
    [self.timer invalidate];
    self.timer = nil;
}
复制代码

使用的时候,我们在目标控制器里声明一个中间者类型的属性。

@interface MyViewController ()
@property (nonatomic, strong) MyTimerMiddle * timerMid;
@end
复制代码

初始化中间类,注意,这里的taerget指定为self,因为之前声明target属性的时候用的是weak,因此这里中间类并没有强引用self,从而避免了循环引用。showSome即是目标对象最终要定时执行的任务。

self.timerMid = [[MyTimerMiddle alloc] initWithTimeInterval:5 target:self selector:@selector(showSome:)];
复制代码

最后,我们需要在控制器销毁时对中间类进行清理工作。

- (void)dealloc {
    NSLog(@"111");
    [self.timerMid cleanup];
}
复制代码

猜你喜欢

转载自juejin.im/post/7031173989305155592