解决NSTimer循环引用导致内存泄漏的六种方法

4,182 阅读4分钟

demo放在了GitHub

参考另一篇文章可加深印象NSTimer的八种创建方式

另一篇文章对第三种方法做了重点解释让大家拿过来就能用且没有循环引用的定时器TFQWeakTimer

内存泄漏的原因:

self强引用timertimer添加在runloop上,只要timer不销毁self就销毁不了。当然了你可以选择在viewWillDisappear中销毁timer。但是定时器页面不一定都是pop到上一个页面,也有可能push新页面,也有可能是进入后台,这样我们希望重新回到定时器页面的时候,定时任务还依旧是执行状态。所以invalidate放到viewWillDisappear是不合理的,唯一合理的地方就是定时器页面销毁的时候销毁timer。这就引出了以下三种解决方法。

第一、二种方法是在合适的时机销毁timer,干掉强引用。

第三种方法是自定义timer,弱引用timer,从源头上就不形成循环引用,更不会导致内存泄漏。

一、离开当前控制器销毁NSTimer

didMoveToParentViewController方法了解一下

@interface WLZSecondController ()

@property (nonatomic, strong)UILabel *label;
@property (nonatomic, assign)int repeatTime;
//第一、二种的属性
//@property (nonatomic, strong)NSTimer *timer;
//第三种方法的属性
@property (nonatomic, strong)WLZTimer *timer;

@end

@implementation WLZSecondController


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor yellowColor];
    self.repeatTime = 60;
    self.label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 50)];
    self.label.text = @"60";
    self.label.textColor = [UIColor blackColor];
    [self.view addSubview:self.label];
    [self createTimer];
}

#pragma mark - 第一种方法
- (void)createTimer{
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(change) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if(parent == nil){
        [self.timer invalidate];
    }
}

- (void)change{
    self.repeatTime --;
    self.label.text = [NSString stringWithFormat:@"%d",self.repeatTime];
    if(self.repeatTime == 0){
        [self.timer invalidate];
    }
}

- (void)dealloc{
    NSLog(@"dealloc");
}

二、自定义返回按钮销毁NSTimer

这种方法就需要禁止掉侧滑返回手势。

#pragma mark - 第二种方法  这里只是随意创建了一个button,具体的图片、文案可以自己调试。
- (void)createTimer{
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(change) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)viewWillAppear:(BOOL)animated{
    [self changeBackBarButtonItem];
}

- (void)changeBackBarButtonItem{
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(invalidateTimer)];
}

- (void)invalidateTimer{
    [self.timer invalidate];
    [self.navigationController popViewControllerAnimated:YES];
}

- (void)change{
    self.repeatTime --;
    self.label.text = [NSString stringWithFormat:@"%d",self.repeatTime];
    if(self.repeatTime == 0){
        [self.timer invalidate];
    }
}

- (void)dealloc{
    NSLog(@"dealloc");
}

三、自定义timer,压根不形成循环引用

自定义timer类

.h文件
@interface WLZTimer : NSObject

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

///创建timer
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval Target:(id)target andSelector:(SEL)selector;
///销毁timer
- (void)closeTimer;

@end

.m文件
@interface WLZTimer ()

@property (nonatomic, strong)NSTimer *timer;

@end

@implementation WLZTimer

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

- (void)dosomething{
    //这里是为了不阻塞主线程
    dispatch_async(dispatch_get_main_queue(), ^{
        id target = self.target;
        SEL selector = self.selector;
        if([target respondsToSelector:selector]){
            [target performSelector:selector withObject:nil];
        }
    });
}

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

- (void)dealloc{
    NSLog(@"WLZTimer dealloc");
}

@end

自定义timer在主线程异步执行任务不明白原因的话,可参考文章iOS线程、同步异步、串行并行队列

创建timer的时候就用自定义的类就可以

#pragma mark - 第三种方法
/*
 *  与第前两种不同的是:前两种只是在合适的时机解决循环引用,
 *  第三种根本就不会造成循环引用,可以封装起来供多个地方使用,而且遵循单一职责原则
 *
 */
- (void)createTimer{
    self.timer = [[WLZTimer alloc] initWithTimeInterval:1 Target:self andSelector:@selector(change)];
}

- (void)change{
    self.repeatTime --;
    self.label.text = [NSString stringWithFormat:@"%d",self.repeatTime];
    if(self.repeatTime == 0){
        [self.timer closeTimer];
    }
}

- (void)dealloc{
    [self.timer closeTimer];
    NSLog(@"dealloc");
}

2019/1/17 更新

四、定义中介继承NSObject进行消息转发消除强引用NSTimer

+ (instancetype)proxyWithTarget:(id)aTarget{
    TFQProxy *proxy = [[TFQProxy alloc] init];
    proxy.target = aTarget;
    return proxy;
}

//自己不能处理这个消息,就会调用这个方法来消息转发,return target,让target来调用这个方法。
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if([self.target respondsToSelector:aSelector]){
        return self.target;
    }
    return nil;
}
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TFQProxy proxyWithTarget:self] selector:@selector(repeatAction:) userInfo:nil repeats:YES];

五、定义中介继承NSProxy进行消息转发消除强引用NSTimer

+ (instancetype)proxyWithTarget:(id)aTarget{
    TFQProxySubclass *proxy = [TFQProxySubclass alloc];
    proxy.target = aTarget;
    return proxy;
}

//为另一个类实现的消息创建一个有效的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

//将选择器转发给一个真正实现了该消息的对象
- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TFQProxySubclass proxyWithTarget:self] selector:@selector(repeatAction:) userInfo:nil repeats:YES];

tips:五比四效率高,因为5是系统的类,直接进行消息转发,4会走几条弯路才会到消息转发那个方法

六、GCD创建定时器

/**
 GCD创建定时器

 @param task 定时器内容
 @param interval 执行间隔
 @param repeat 是否重复
 @param async 是否异步
 @param identifier 定时器唯一ID
 @return 返回定时器唯一ID,销毁的时候用
 */
+ (NSString *)schedleTask:(void (^)(void))task interval:(NSTimeInterval)interval repeat:(BOOL)repeat async:(BOOL)async reuseIdentifier:(NSString *)identifier{
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    //穿件定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //开始时间
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 0*NSEC_PER_SEC);
    //设置各种时间
    dispatch_source_set_timer(timer, start, interval*NSEC_PER_SEC, 0);
    //设置回调
    dispatch_source_set_event_handler(timer, ^{
        task();
        if(!repeat){
            [self cancelTimer:identifier];
        }
    });
    //启动定时器
    dispatch_resume(timer);
    timerDict[identifier] = timer;
    return identifier;
}

+ (void)cancelTimer:(NSString *)identifier{
    dispatch_source_cancel(timerDict[identifier]);
    [timerDict removeObjectForKey:identifier];
}
__weak typeof(self) weakSelf = self;
self.timerIdentifier = [TFQGCDTimer schedleTask:^{
    [weakSelf repeatAction:nil];
} interval:1 repeat:YES async:NO reuseIdentifier:@"identifier"];