NSTimer特别容易出现内存泄露问题,这篇文章会分析一下为什么会出现内存泄露,以及如何优雅的解决这个问题。
NSTimer导致内存问题的原因分析
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
_timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
- (void)timerAction {}
@end
这段代码是常见的NSTimer使用方法,然而这段代码会有一个隐患,self
无法正常被释放,除非在self
的dealloc
执行之前调用[_timer invalidate]
,self
才能正常被释放。此时他们在内存中的引用关系如图
我这个例子用的
strong
修饰timer
,所以self
强引用着timer
, 同时timer
也会强引用self
,timer
运行需要添加到当前线程所运行的runloop
中,所以runloop
也强引用着timer
。当self
被pop掉的时候,此时self
始终被timer
强引用着,从而无法在内存中释放,这也是为什么不能在self
的dealloc
方法中去做[_timer invalidate]
的原因。
最笨的解决方法:在dealloc前手动去释放
上面这段代码,已经出现了内存泄露,只要app没被杀死,self
会一直在内存中,那么有种最笨的方法就是手动去释放,例如可以在- (void)viewWillDisappear:(BOOL)animated
方法中去做[_timer invalidate]
操作,执行完invalidate
后,此时runloop
不在持有timer
, timer
也不在持有self
,即图中的2和3箭头断开,此时没有任何对象引用着self
,self
的引用计数为0,从而能够在内存中被释放,self
被释放后,timer
此时也没对象引用着,即1箭头断开,从而timer
也在内存中被释放。
当然这种方法也是会受到一定的业务限制的,例如self
上继续push一个viewController的时候,runloop
不在引用timer
, timer
不在引用self
, 此时self
、timer
都在内存中,只不过timer
不在运行着了。假如业务场景要求push的时候,timer
正常工作,只有self
pop的时候才杀掉timer
,此时这种处理方式就显得力不从心了。
那么有什么通用方案能让使用者不在担心内存问题吗
根据上面的分析,我们知道,真正导致self
无法释放的原因是timer
强引用着,那么如果说timer
不强引用self
的话,self
就能在pop的时候愉快的释放了,开发者也能够在self
的dealloc
方法中去做[_timer invalidate]
操作杀死timer
了。
于是在这种思路下,我做了如下尝试
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
__weak typeof(self) weakSelf = self;
_timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
- (void)timerAction {}
@end
这段代码和上面的代码只有self和weakSelf的差别,此时内存中的引用关系如图
注意虚线这里表示弱引用,
self
的引用计数不+1的。当self
被pop的时候,由于没有其它对象引用着他,所以能够正常释放,从而能够正常执行self
的dealloc
方法,此时1箭头断开,在self
的dealloc
方法中执行[_timer invalidate]
,此时3和2断开,timer
也从内存中被释放。看起来没有任何地方违背理论基础,貌似一切大功告成。然后通过实际调试测试发现,虽然我试图让
timer
持有weakSelf
,然而实际并没生效,timer
依然会强引用self
。
所以这种尝试失败,不过具体更深层次的原因我还不知道,我只知道我想让timer强引用weakSelf
,而timer依然强引用了self
, 可能苹果出于安全考虑,为了确保执行事件的target始终存在,在内部做了什么特殊操作,因为self
一旦被释放,weakSelf
将会指向nil, timer
的target对象也就为nil了。
终极解决姿势
说道终极解决姿势得感谢github开源代码的各路豪杰,我也是在第三方库中看到的解决方案。这里其实就是通过消息转发机制巧妙的解决了这个问题。
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
_timer = [NSTimer timerWithTimeInterval:1 target:[JLWeakProxy proxyWithTarget:self] selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
- (void)timerAction {}
@end
此时他们在内存中的引用关系如图
timer内存引用关系简图
这里的核心点就在JLWeakProxy
上,timer
的直接target
对象其实是JLWeakProxy
, 也就是说,timer其实是通过JLWeakProxy对象去执行selector方法的,只不过在JLWeakProxy
内部,弱引用着self
, 并且会把消息转发给self
去执行selector
方法。
从内存引用简图可以看出,popself
的时候,self
能够正常释放,从而dealloc
方法能够正常被执行,在dealloc
中做[_timer invalidate]
操作,此时runloop
不在引用timer, timer 也不在引用JLWeakProxy
,从而内存也就都被释放了。
关于消息转发这里不做解释说明,感兴趣的可以看看我整理的iOS Runtime: 消息转发,下面贴出JLWeakProxy
实现。
@interface JLWeakProxy : NSProxy
@property (nullable, nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation JLWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[JLWeakProxy 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
网友评论