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 强引用了,如果能打破这个强引用,问题应该就能决了。
问题解决:
(查阅到sunnyxx和TEASON有写到过相关问题的原理及解决方案)我们可以造一个假的 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
---玩的酷,靠得住
仅做个人学习记录所用,侵删。