NSTimer循环引用的几种解决方案
NSTimer在使用中需要注意,避免循环引用的问题。但是需要纠正一种说法
注意:timer内存泄漏,部分童鞋认为是vc与timer循环引用造成的,这种说法是错误的!
@interface ViewController ()
@property (nonatomic, weak) NSTimer *timer;
@end
- (void)viewDidLoad {
[super viewDidLoad];
{
NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:nil repeats:YES];
_timer = one;
}
}
timer强引用target,即timer强引用vc,然而vc并没有强引用timer,哪来的vc与timer循环引用?但是,如果vc没有强引用timer,timer是如何存活的?
其实,默认将timer加入到currentRunLoop中,currentRunLoop会强引用timer,而currentRunLoop就是mainRunLoop,mainRunLoop一直存活,所以timer可以存活
关系可参考如下图示:

从以上关系图可见,只要runLoop存活,vc必然存活,所以vc的dealloc方法自然就不会执行。因此,将timer的销毁方法放在dealloc中必然造成内存泄漏!
虽然NSRunLoop提供了addTimer接口,但是并没有提供removeTimer接口,显然,runLoop与timer这条线无法直接销毁,所以只能从vc与timer持有关系入手。
内部其实对target对象做了retain操作, _target = RETAIN(object);
- (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);
_selector = selector;
_info = RETAIN(info);
if (f == YES)
{
_repeats = YES;
_interval = ti;
}
else
{
_repeats = NO;
_interval = 0.0;
}
return self;
}
调用invalidate的时候,会执行destroy操作
- (void) invalidate
{
/* OPENSTEP allows this method to be called multiple times. */
_invalidated = YES;
if (_target != nil)
{
DESTROY(_target);
}
if (_info != nil)
{
DESTROY(_info);
}
}
下面介绍一下几种解决timer循环引用的方法。
1. 选择合适的时机手动释放timer
调用[self.timer invalidate];
2. timer使用block方式添加Target-Action
这里我们需要自己在NSTimer的分类中添加类方法:
@interface NSTimer (YYAdd)
/**
Creates and returns a new NSTimer object and schedules it on the current run
loop in the default mode.
@discussion After seconds seconds have elapsed, the timer fires,
sending the message aSelector to target.
@param seconds The number of seconds between firings of the timer. If seconds
is less than or equal to 0.0, this method chooses the
nonnegative value of 0.1 milliseconds instead.
@param block The block to invoke when the timer fires. The timer maintains
a strong reference to the block until it (the timer) is invalidated.
@param repeats If YES, the timer will repeatedly reschedule itself until
invalidated. If NO, the timer will be invalidated after it fires.
@return A new NSTimer object, configured according to the specified parameters.
*/
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats;
/**
Creates and returns a new NSTimer object initialized with the specified block.
@discussion You must add the new timer to a run loop, using addTimer:forMode:.
Then, after seconds have elapsed, the timer fires, invoking
block. (If the timer is configured to repeat, there is no need
to subsequently re-add the timer to the run loop.)
@param seconds The number of seconds between firings of the timer. If seconds
is less than or equal to 0.0, this method chooses the
nonnegative value of 0.1 milliseconds instead.
@param block The block to invoke when the timer fires. The timer instructs
the block to maintain a strong reference to its arguments.
@param repeats If YES, the timer will repeatedly reschedule itself until
invalidated. If NO, the timer will be invalidated after it fires.
@return A new NSTimer object, configured according to the specified parameters.
*/
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats;
@end
通过block的方式,获取action,实际的target设置为self,即NSTimer类。这样我们在使用timer时,由于target的改变,就不再有循环引用了。 使用中还需要注意block可能引起的循环引用,所以使用weakSelf:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 block:^{
[weakSelf dosomething];
} repeats:YES];
#import "NSTimer+YYAdd.h"
#import "YYCategoriesMacro.h"
YYSYNTH_DUMMY_CLASS(NSTimer_YYAdd)
@implementation NSTimer (YYAdd)
+ (void)_yy_ExecBlock:(NSTimer *)timer {
if ([timer userInfo]) {
void (^block)(NSTimer *timer) = (void (^)(NSTimer *timer))[timer userInfo];
block(timer);
}
}
+ (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];
}
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
return [NSTimer timerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];
}
@end
虽然没有了循环引用,但是还是应该记得在dealloc时释放timer。
3. 通过proxy转发的形式解决
如图:

考虑到循环引用的原因,改方案就是需要打破这些相互引用关系,因此添加一个中间件,弱引用self,同时timer引用了中间件,这样通过弱引用来解决了相互引用
proxy弱引用vc,所以vc可以释放,当vc执行dealloc,在dealloc内部销毁timer即可
参考:YYKit解决方案:
@interface YYWeakProxy : NSProxy
/**
The proxy target.
*/
@property (nullable, nonatomic, weak, readonly) id target;
/**
Creates a new weak proxy for target.
@param target Target object.
@return A new proxy object.
*/
- (instancetype)initWithTarget:(id)target;
/**
Creates a new weak proxy for target.
@param target Target object.
@return A new proxy object.
*/
+ (instancetype)proxyWithTarget:(id)target;
@end
#import "YYWeakProxy.h"
@implementation YYWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
- (BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
- (NSUInteger)hash {
return [_target hash];
}
- (Class)superclass {
return [_target superclass];
}
- (Class)class {
return [_target class];
}
- (BOOL)isKindOfClass:(Class)aClass {
return [_target isKindOfClass:aClass];
}
- (BOOL)isMemberOfClass:(Class)aClass {
return [_target isMemberOfClass:aClass];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_target conformsToProtocol:aProtocol];
}
- (BOOL)isProxy {
return YES;
}
- (NSString *)description {
return [_target description];
}
- (NSString *)debugDescription {
return [_target debugDescription];
}
@end