NSTimer
或CADisplayLink
不小心处理的话,极易造成循环引用。不管target使用weak
还是strong
修饰,timer都会对target强引用,而runloop本身也会对timer强引用,造成runloop引用timer,timer引用target的情况。如果target是控制器的话,控制器就不能释放。
要解决这个问题,就要打断这种引用链条。
1、在viewWillDisappear里处理
在viewWillDisappear
里面,调用timer的invalidate
方法。
不足:push进入其他控制器页面时,本页面也会调用timer的invalidate
方法,造成timer失效。
2、在willMoveToParentViewController里处理
在willMoveToParentViewController
方法里,对timer做invalidate操作。如果控制器外面是容器控制器,进入控制器时和返回上一个控制器时会调用该方法,我们在返回上一个控制器时,调用timer的invalidate
方法。
- (void)willMoveToParentViewController:(UIViewController *)parent {
if (self.viewLoaded) {
if (_timer.valid) {
[_timer invalidate];
_timer = nil;
}
}
}
不足:只有当控制器外层是容器控制器时,例如UINavigationController
,才能使用此方法。
3、将timer的target指向中介者
使用中介者,将timer的target指向这个中介者。 当timer调用中介者的响应方法时,我们通过消息转发机制,让控制器去实际响应这个方法。这样一来,控制器强引用timer, timer强引用中介者, 中介者弱引用控制器,控制器可以正常释放,然后在控制器的dealloc
里面调用timer的invalidate
方法。
中介者可以继承自NSObject
或者NSProxy
。继承自NSObject
时,若在当前类找不到需要调用的方法,要走完整的消息查找流程和转发流程。而继承自NSProxy
时,则直接触发转发流程,省去了去父类查找方法的过程,省去了动态方法解析的过程,效率比使用NSObject
高。
ViewController.m:
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TTRealProxy proxyWithTarget:self] selector:@selector(timerFire) userInfo:nil repeats:YES];
- (void)dealloc {
[_timer invalidate];
}
TTRealProxy.h:
@interface TTRealProxy : NSProxy
//弱引用
@property (nonatomic, weak) id target;
+ (id)proxyWithTarget:(id)target;
@end
TTRealProxy.m:
@implementation TTRealProxy
+ (id)proxyWithTarget:(id)target {
TTRealProxy *proxy = [TTRealProxy alloc];
proxy.target = target;
return proxy;
}
////若不实现此方法, 则直接进入消息转发流程
//- (void)timerFire {
// NSLog(@"timerFire");
//}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [_target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:_target];
}
@end
不足:比较繁琐。
4、将timer的target指向类对象
将timer的target指向NSTimer类对象。类对象不用考虑引用和释放的问题。由于没有对控制器强引用,控制器可以正常释放,然后在控制器的dealloc
方法里面调用timer的invalidate
方法。
参考YYKit的实现:
// NSTimer+YYAdd.m
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
return [NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];
}
+ (void)_yy_ExecBlock:(NSTimer *)timer {
if ([timer userInfo]) {
void (^block)(NSTimer *timer) = (void (^)(NSTimer *timer))[timer userInfo];
block(timer);
}
}
5、使用带block的API
使用带block的API。 不需要使用target,就不需要对控制器进行引用了。
不足:只有NStimer
有带block的API,CADisplayLink
是没有的。
总结
综上所述,第4和第5是最佳解决方法。