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来打破相互引用关系,使彼此都能够释放;说到底就是引用计数的问题,内存管理需要遵循:谁申请、谁添加、谁释放的原则