iOS学习小集iOS DeveloperiOS开发

Controller销毁NSTimer释放的细节

2016-10-17  本文已影响902人  解忧杂货店老板

关于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

                                                               ---玩的酷,靠得住

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

上一篇下一篇

猜你喜欢

热点阅读