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

                                                               ---玩的酷,靠得住

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容

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