计时器是一种很方便也很有用的对象, Foundation 框架中有一个类叫做 NSTimer ,开发者可以指定绝对的日期与时间,以便到时执行任务, 也可以指定执行任务的相对延迟时间,
计时器还可以重复运行任务, 有个与之相关联的 '间隔值'可以用力爱指定任务的触发频率.
计时器要和'运行循环'相关联,运行循环到时候会触发任务, 创建 NSTimer 时, 可以将其 '预先安排'在当前的运行循环中, 也可以先创建好, 然后由开发者自己来调度, 无论采用哪种方式, 只有把计时器放在运行循环里面, 它才能正常触发任务.
例如: + (NSTimer *)scheduleTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;
用此方法创建出来的计时器, 会在指定的间隔时间之后执行任务, 也可以令其反复执行任务, 知道开发者稍后将其手动关闭为止, target 与 selector 参数表示计时器将在那个对象上调用哪个方法, 计时器会保留其目标对象, 等到自身'失效'时再释放此对象, 调用 invalidate 方法可令计时器失效, 执行完相关任务之后,一次性的计时器也会失效, 开发者若将计时器设置成重复执行模式, 那么必须自己调用 invalidata 方法, 才能令其失效.
由于计时器会保留其目标对象, 所以反复执行任务通常会导致应用程序出问题, 也就是说, 设置成重复执行模式的那种计时器, 很容易引入'保留环',
#import@interface ClassA : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
@implementation ClassA{
NSTimer * _pollTimer;
}
- (id)init{
return [super init];
}
- (void)dealloc{
[_pollTimer invalidate];
}
- (void)stopPolling{
[_pollTimer invalidate];
_pollTimer = nil;
}
- (void)startPolling{
_pollTimer = [NSTimer scheduleTimerWithTimeInterval:5.0 taeget:self selector:@selector(p_dePoll) userInfo:nil repeates:YES];
}
- (void)p_dePoll{
//////
}
如果创建了本类的实例, 并调用其 startPolling 方法, 那么如何呢? 创建计时器的时候, 由于目标对象是 self, 所以要保留此实例,然而.因为计时器是用实例变量存放的, 所以实例也保留计时器, 于是,就产生了 '保留环', 如果此环 能在某一刻打破, 那就不会出什么情况, 然而要想打破保留环, 只能改变实例变量 或 令计时器无效, 所于说, 要么调用 stopPolling ,要么令系统将此实例回收. 而且即便能满足此条件, 这种通过调用某方法来避免内存泄漏的做法, 也不是一个好主意, 另外, 如果想在系统回收本类实例的过程中令计时器无效, 那又会陷入死结, 因为在计时器尚且有效时, ClassA 实例的保留计数 绝不会降为 0, 因此系统也绝不会将其回收, 而现在又没人来调用 invalidate 方法, 所以计时器将已知处于有效状态.
当指向 ClassA 实例的最后一个外部引用移走之后, 该实例仍然会继续存活, 因为计时器还保留着它, 而计时器对象也不可能为系统所释放, 因为实例中还有一个 强引用 正在指向它, 于是,内存就泄漏了, 这种内存泄漏问题尤为严重, 因为计时器还将反复地执行轮询任务.
但从计时器本身入手, 很难解决这个问题, 这个问题可以用 '块'来解决.
scheduledTimerWithTimeInterval:1.0 repeats:(BOOL) block:^(NSTimer * _Nonnull timer) {
}
这个办法为何能解决 '保留环'问题呢? 这段代码将计时器所应执行的任务 封装成 '块' ,在调用计时器函数时, 只要计时器还有效, 就会一直保留着它, 然后在快里面采用 弱引用 调用方法. 使 块 捕获这个引用, 而不直接捕获普通的 self 变量, 也就是说, self 不会为计时器所保留.'=
采用这种写法之后, 如果外界指向 ClassA 实例的最后一个引用将其释放, 则该实例就可为系统所回收了, 回收过程中还会调用计时器的 invalidata 方法, 这样的话,计时器就不会再执行任务了, 此处使用 weak 引用还能令程序更加安全, 因为有时开发者可能在编写 dealloc 时忘了调用计时器的 invalidate 方法.从而导致计时器再次运行, 若发生这种情况, 则 块 里面的 weakSelf 会变成 nil.
总结:
NSTimer 对象会保留其目标, 知道计时器本身失效为止, 调用 invalidate 方法可 令计时器失效, 另外,一次性的计时器在触发任务完成之后也会失效.
反复执行任务的计时器 ,很容易引入 保留环 ,如果这种计时器的目标对象又保留了计时器本身, 那肯定会导致 保留环, 这种环状保留关系, 可能是直接发生的, 也可能是通过其他对象间接发生的.
可以扩充 NSTimer 的功能, 用 '块'来打破 保留环,不过.