NSTimer 内存管理 - AlvinSunny/OC-TheUnderlying GitHub Wiki
NSTimer循环引用
这样一份代码会产生循环引用,其原因是因为scheduledTimerWithTimeInterval内部对target做了一次 retain 操作,使其引用计数加一;NSRunLoop还会对定时器timer产生一个强引用
@property (nonatomic, strong) NSTimer *timer;
//scheduledTimerWithTimeInterval方法内部会启动Runloop,自动开始定时任务
self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
- (void)timerAction {
NSLog(@"执行定时任务");
}
看看scheduledTimerWithTimeInterval:target:selector: userInfo: repeats的内部实现
+ (NSTimer*) scheduledTimerWithTimeInterval: (NSTimeInterval)ti
target: (id)object
selector: (SEL)selector
userInfo: (id)info
repeats: (BOOL)f
{
id t = [[self alloc] initWithFireDate: nil
interval: ti
target: object
selector: selector
userInfo: info
repeats: f];
//自动开启runloop
[[NSRunLoop currentRunLoop] addTimer: t forMode: NSDefaultRunLoopMode];
RELEASE(t);
return t;
}
- (id) initWithFireDate: (NSDate*)fd
interval: (NSTimeInterval)ti
target: (id)object
selector: (SEL)selector
userInfo: (id)info
repeats: (BOOL)f
{
if (ti <= 0.0) {
ti = 0.0001;
}
if (fd == nil){
_date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
initWithTimeIntervalSinceNow: ti];
}else {
_date = [fd copyWithZone: NSDefaultMallocZone()];
}
//关键代码 😁 ✅
_target = RETAIN(object); // #define RETAIN(object) [(id)(object) retain]
_selector = selector;
_info = RETAIN(info);
if (f == YES)
{
_repeats = YES;
_interval = ti;
}
else
{
_repeats = NO;
_interval = 0.0;
}
return self;
}
NSRunLoop对定时器timer产生一个强引用实现逻辑
#define GSI_ARRAY_RETAIN(A, X) [(X).obj retain]
GS_STATIC_INLINE void GSIArrayAddItem(GSIArray array, GSIArrayItem item) {
GSI_ARRAY_CHECK;
GSI_ARRAY_RETAIN(array, item);//核心 🎉🎉
if (array->count == array->cap)
{
GSIArrayGrow(array);
}
array->ptr[array->count++] = item;
GSI_ARRAY_CHECK;
}
如何解决这个问题呢 ?
方法一: 在控制器即将推出释放定时器
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
关于invalidate的官方解释:
中文翻译:
-
停止计时器再次触发,并请求将其从运行循环中移除。
-
这个方法是唯一从NSRunLoop对象中删除计时器的方法。NSRunLoop对象移除它对计时器的强引用,要么在invalidate方法返回之前,要么在稍后的某个时间点。
-
如果配置了目标和用户信息对象,接收方(NSTimer)也会删除对这些对象的强引用。
-
必须从安装计时器的线程发送此消息。如果您从另一个线程发送此消息,与计时器相关的输入源可能不会从其运行循环中删除,这可能会阻止线程正确退出。
方法二:
iOS 10 之后使用block方式创建Timer, 官方推荐
方法三: 使用中间者(NSP)做一个中转
总结: 强引用不可怕可怕的是,强引用之后不释放;这样会导致内存泄漏;就拿自定义block的循环引用来说,使用__block解决循环引用的原理就是再合适的时机置为nil来打破相互引用关系,使彼此都能够释放;说到底就是引用计数的问题,内存管理需要遵循:谁申请、谁添加、谁释放的原则