Controller销毁NSTimer释放的细节

关于NSTimer释放和内存泄漏的问题。

@(NSTimer)[内存管理,NSTimer释放,循环引用]

首先需要再次明确最基础的iOS引用计数内存管理模式(按照说人话的方式):
1)自己生成的对象,自己所持有。
2)非自己生成的对象,自己也能持有。
3)自己持有的对象不再需要时被释放。
4)非自己持有的对象无法释放。


坑出现的地方

美工要求在签到按钮上显示一个时间的label,于是就封装了一个专门用于显示时间TimeLabel;


签到TimeLabel
@interface FMTimeLabel ()
@property (nonatomic, weak) NSTimer *timer;
@end

@implementation FMTimeLabel

- (instancetype)init {
    self = [super init];
    if (self) {
        //用NSTimer每秒循环执行updateTime方法,达到更新label显示内容的目的。
        _timer = [NSTimer scheduledTimerWithTimeInterval:1
                                                  target:self
                                                selector:@selector(updateTime)
                                                userInfo:nil
                                                 repeats:YES];
        [self updateTime];
    }
    return self;
}

- (void) updateTime {
    //每次执行时,获取当前时间的 时 分 秒 转化成string类型给label显示
    NSString *time = [FMUtils getTimeDescriptionByDate:[NSDate date] format:@"hh:mm:ss"];
    [self setText:time];
}

- (void)dealloc {
    if (_timer) {
        if ([_timer isValid]) {
            [_timer invalidate];
            _timer = nil;
        }
    }
}

@end

此时一切都很美好功能实现了效果也ok,但是多次刷新TableView之后,问题出现了。

timelabel直接卡死不再刷新时间,并且也不走dealloc方法。

排查原因:内存泄漏,TimeLabel持有的NSTimer没有被释放,导致TimeLabel也不能被释放,从而导致线程挂起的状态。

填坑的方法

问题分析:

原因是 Timer 添加到 Runloop 的时候,会被 Runloop 强引用,然后 Timer 又会有一个对 Target 的强引用(也就是 self )也就是说 NSTimer 强引用了 self ,导致 self 一直不能被释放掉,所以 self 的 dealloc 方法也一直未被执行.

知道了错误原因,就先查一下NSTimer的官方文档看看具体用法细节,发现NSTimer还有一个规则:(在哪个线程创建就要在哪个线程停止,否则会导致资源不能被正确的释放。)那么问题来了:如果我就是想让这个 NSTimer 一直输出,直到 CustomerViewController 销毁才停止并且释放NSTimer。

问题关键:

问题的关键就在于 self 被 NSTimer 强引用了,如果能打破这个强引用,问题应该就能决了。

问题解决:

(查阅到sunnyxxTEASON有写到过相关问题的原理及解决方案)我们可以造一个假的 target 给 NSTimer 。这个假的 target 类似于一个中间的代理人,它做的唯一的工作就是挺身而出接下了 NSTimer 的强引用。(这个解决方案甚是巧妙)然后在self释放的时候随self一起释放,然后层层解扣,达到在ViewController销毁的时候释放NSTimer,这个target类声明如下:(摘自TEASON)

@interface HWWeakTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end
@implementation HWWeakTimerTarget
 -(void) fire:(NSTimer *)timer {
    if(self.target) {
        [self.target performSelector:self.selector withObject:timer.userInfo];
    } else {
        [self.timer invalidate];
    }
}
@end

然后再封装一个假的NSTimer的方法 scheduledTimerWithTimeInterval 方法,但是在调用的时候已经偷梁换柱了:

+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(id)userInfo
                                     repeats:(BOOL)repeats {
    HWWeakTimerTarget* timerTarget = [[HWWeakTimerTarget alloc] init];
    timerTarget.target = aTarget;
    timerTarget.selector = aSelector;
    timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                         target:timerTarget
                                                       selector:@selector(fire:)                                                                                                           
                                                       userInfo:userInfo
                                                        repeats:repeats];
    return timerTarget.timer;
}

至此将原来NSTimer换成封装好的CustomerTimer再次运行,问题解决。

FMTimer.h

#import <Foundation/Foundation.h>

typedef void (^FMTimerHandler)(id userInfo);

@interface FMWeakTimer : NSObject

+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(id)userInfo
                                     repeats:(BOOL)repeats;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      block:(FMTimerHandler)block
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats;

@end

FMTimer.m

#import "FMWeakTimer.h"

@interface FMWeakTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end

@implementation FMWeakTimerTarget
- (void) fire:(NSTimer *)timer {
    if(self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
#pragma clang diagnostic pop
    } else {
        [self.timer invalidate];
    }
}
@end



@implementation FMWeakTimer

+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(id)userInfo
                                     repeats:(BOOL)repeats{
    FMWeakTimerTarget* timerTarget = [[FMWeakTimerTarget alloc] init];
    timerTarget.target = aTarget;
    timerTarget.selector = aSelector;
    timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                         target:timerTarget
                                                       selector:@selector(fire:)
                                                       userInfo:userInfo
                                                        repeats:repeats];
    return timerTarget.timer;
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      block:(FMTimerHandler)block
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats {
    NSMutableArray *userInfoArray = [NSMutableArray arrayWithObject:[block copy]];
    if (userInfo != nil) {
        [userInfoArray addObject:userInfo];
    }
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(_timerBlockInvoke:)
                                       userInfo:[userInfoArray copy]
                                        repeats:repeats];
    
}

+ (void)_timerBlockInvoke:(NSArray*)userInfo {
    FMTimerHandler block = userInfo[0];
    id info = nil;
    if (userInfo.count == 2) {
        info = userInfo[1];
    }
    if (block) {
        block(info);
    }
}

@end

                                                               ---玩的酷,靠得住

仅做个人学习记录所用,侵删。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 1.NSTimer的介绍 (1.)8种创建方法 <1> + (NSTimer *)timerWithTimeInt...
    liangZhen阅读 12,199评论 0 6
  • 创建NSTimer 创建NSTimer的常用方法是: + (NSTimer *)scheduledTimerWit...
    LanWor阅读 5,237评论 0 2
  • iOS中计时器常用的有两种方式 使用NSTimer类(Swift 中更名为 Timer) NSTimer 常用的初...
    superDg阅读 5,842评论 0 1
  • 燕儿穿过杨柳雨 终觅春风十里 鱼儿游戏芙蓉 嘻见叶落波起 清风徐徐 池水卷了又舒 我笑甄玉斝 何必望桃花 因为有你
    MingHiker阅读 1,526评论 0 0

友情链接更多精彩内容