NSTimer有如下两种基本的使用方式:
1. 创建对象并加入到当前的runloop里
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(fire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
2. 直接使用类方法创建并启动定时器
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"--->> NSTimer block.");
}];
第二种使用方法本质上和第一种是一样的,此方法内部完成了把创建的定时器对象加入runloop的工作。
分析如上的用法,可以发现,假设是在一个控制器里使用上述定时器,控制器持有了定时器对象,定时器对象又会通过对target对象(此时为控制器)的引用,造成循环引用导致内存泄漏。下图红色部分即为循环引用。
无效的解除方法
基于iOS ARC的内存管理模式,在某对象引用计数为0的时候,系统框架会通知对象调用其析构函数dealloc。
但是我们分析上述的循环引用可知,直接在控制器的析构函数调用定时器的失效方法是无法解除循环引用的,因为控制器的引用计数在被定时器引用的情况下,不可能为0,所以dealloc方法不会被调用,也就无法使定时器失效。
//直接在析构函数使定时器失效的方法永远不会被调用
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
NSLog(@"SZTimerViewController dealloc.");
}
在适当的时机主动解除环的某一方的引用
控制器的生命周期方法didMoveToParentViewController,在控制器添加到父控制器和从父控制器移除时都会被系统框架通知控制器调用,所以可以重写此方法,判断在父控制器参数为nil时(控制器从父控制器移除),将定时器失效。
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (parent == nil) {
[self.timer invalidate];//调用此方法,定时器解除对target的引用和runloop解除对定时器的引用;
self.timer = nil;//不置nil,偶尔会有timer没释放的情况
}
}
当退出当前页面时,强引用的解除顺序是:
效果如下图所示。
注意:不能在控制器的其他生命周期方法如,viewDidDisappear:方法等来做定时器失效的工作,只能是在控制器移除的方法里做,因为其他方法如viewDidDisappear,当从当前控制器页面进入下一级页面时,一般不希望定时器失效。
使用runtime运行时引入第三方对象作为定时器的target
原理是,换一个target对象,不与定时器的持有者形成环引用,运用runtime运行时将定时器处理方法添加到新的target对象,将处理方法发送到新的target处理。
对象之间不再有循环引用,则控制器的析构函数可以被调用,在其析构函数调用定时器失效方法即可。引用关系如下图所示。
实现方法如下:
// 2.1 换一个timer的target,不与self形成环引用,并使用运行时结束为新的target新增定时器的定时处理方法
self.timerTarget = [NSObject new];
class_addMethod([self.timerTarget class], @selector(fire), (IMP)fireImp, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.timerTarget selector:@selector(fire) userInfo:nil repeats:YES];
//2.2 定时器定时要处理的工作
void fireImp(id self, SEL _cmd) {
NSLog(@"--->> fireImp");
}
- (void)dealloc {
// 2.3在控制器销毁前,使定时器失效
[self.timer invalidate];
self.timer = nil;
NSLog(@"SZTimerViewController dealloc.");
}
强引用解除顺序是:
使用消息转发重定向引入第三方对象作为定时器的target
思路本质上与上述第二种方式一样,都是通过引入第三方对象做target,解除引用成环的问题。
与第二种方式思路略有不同的是,引入的第三方target会弱引用控制器对象,给第三方target的方法消息重定向至控制器时使用。
对象之间不再有循环引用,则控制器的析构函数可以被调用,在其析构函数调用定时器失效方法即可。引用关系如下图。
实现方法如下:
// 3.1 新建proxyTarget类
@interface SZTimerProxyTarget : NSProxy
//消息重定向的处理对象
@property (nonatomic, weak) id target;
@end
// 3.2 将方法消息重定向给控制器处理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
// 3.3 使用proxy对象作为target,将向proxy target发送的定时器处理消息,通过在proxy target里进行消息转发重定向,重定向至本定时器所在的控制器对象的方法来处理
self.proxyTarget = [SZTimerProxyTarget alloc];
self.proxyTarget.target = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxyTarget selector:@selector(fire) userInfo:nil repeats:YES];
// 3.4 在控制器销毁前,使定时器失效
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
NSLog(@"SZTimerViewController dealloc.");
}
强引用解除顺序是: