美文网首页
第五十二条: 别忘了NSTimer会保留其目标对象

第五十二条: 别忘了NSTimer会保留其目标对象

作者: iOS博仔 | 来源:发表于2021-12-13 19:28 被阅读0次

    计时器是一种很方便也很用的对象。Foundation框架中有个类叫做NSTimer,开发者可以指定绝对的日期与时间,以便到时执行任务,也可以指定执行任务的相对延迟时间。计时器可以重复运行任务,有个与之相关的"间隔值"(interval)可用来指定任务的触发频率。比方说,可以每5秒轮询某个资源。
    计时器要和"运行循环"(run loop)相关联,运行循环到时候会触发任务。创建NSTimer时,可以将其"预先安排"在当前的运行循环中,也可以先创建好,然后由开发者自己来调度。无论采用哪种方式,只有把计时器放在运行循环里,它才能正常触发任务。例如,下面这个方法可以创建计时器,并将其预先安排在当前运行循环中:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeat

    用此方法创建出来的计时器,会在指定的间隔时间之后执行任务。也可以令其反复执行任务,直到开发者稍后将其手动关闭为止。target与selector参数表示计时器将在哪个对象上调用哪个方法。计时器会保留其目标对象,等到自身“失效”时再释放此对象。调用invalidate方法可令计时器失效;执行完相关任务之后,一次性的计时器也会失效。开发者若将计时器设置成重复执行模式,那么必须自己调用invalidate方法,才能令其停止。
    由于计时器会保留其目标对象,所以反复执行任务通常会导致应用程序出问题。也就是说,设置成重复执行模式的那种计时器,很容易引入"保留环"。要想知道其中缘由,请看下列代码:

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end

@implementation EOCClass {
  NSTimer *_pollTimer;
}

- (id)init {
   return [super init];
}

- (void)dealloc {
  [_pollTimer invalidate];
}

- (void)stopPolling{
  [_pollTimer invalidate];
  _pollTimer = nil;
}

- (void)startPolling{
  _pollTimer = [NSTimer scheduledTimeWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
}

- (void)p_doPoll{
//Poll the resource

}
@end

    能看出问题吗?如果创建了本类的实例,并调用其startPolling方法,那会如何呢?创建计时器的时候,由于目标对象是self,所以要保留此实例。然而,因为计时器是用实例变量存放的,所以实例也保留了计时器。(回想一下,第30条说过,在ARC环境中,这种情况将执行保留操作。)于是,就成生了"保留环",如果此环能在某一时刻打破,那就不会出什么问题。然而要想打破保留环,只能改变实例变量或令计时器无效。所以说,要么调用stopPolling,要么令系统将此实例回收,只有这样才能打破保留环。除非使用该类的所有代码均在你的掌控之中,否则无法确保stopPolling一定会调用。而且即便能满足此条件,这种通过调用某方法来避免内存泄漏的做法,也不是一个好主意。另外,如果想在系统回收本类实例的过程中令计时器无效,从而打破保留环,那又会陷入死结。因为在计时器对象尚且有效时,EOCClass实例的保留计数绝不会降为0,因此系统也绝不会将其回收。而现在又没人来调用invalidate方法,所以计时器将一直处于有效状态。图7-1演示了此情况。

图 7-1 由于计时器保留其目标对象,而对象又保留计时器,所以导致保留环

    当指向EOCClass实例的最后一个外部引用移走之后,该实例仍然会继续存活,因为计时器还保留着它。而计时器对象也不可能为系统所释放,因为实例中还有个强引用正在指向它。更糟糕的是:除了计时器之外,已经没有别的引用再指向这个实例了,于是该实例就永远"丢失"了。而除了该实例之外,又没有其他引用指向计时器。于是,内存就泄漏了。这种内存泄漏问题尤为严重,因为计时器还将继续反复地执行轮询任务。要是每次轮询时都得联网下载数据的话,那么程序就会一直下载数据,这又更容易导致其他内存泄漏的问题。
    单从计时器本身入手,很难解决这个问题。可以要求外界对象在释放最后一个指向本实例的引用之前,必须先调用stopPolling方法。然而这种情况无法通过代码检测出来,此外,假如该类随着某套公开的API对外发布给其他开发者,那么无法保证他们一定会调用此方法。
    这个问题可通过"块"来解决。虽然计时器当前并不直接支持块,但是可以用下面这段代码为其添加此功能:

#import <Foundation/Foundation.h>

+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;

@end

@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats
{
  return [self scheduleTimerWithTimeInterval:interval target:self selector:@selector:(eoc_blockInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)eoc_blockInvoke:(NSTimer *)timer{
  void (^block)() = timer.userInfo;
  if(block) {
    block();
  }
}

@end

相关文章

网友评论

      本文标题:第五十二条: 别忘了NSTimer会保留其目标对象

      本文链接:https://www.haomeiwen.com/subject/wdksfrtx.html