NSTimer,CADisplayLink内存泄漏问题及解决方案
最近项目里经常用到NSTimer
和CADisplayLink
。之前也知道他们都会有内存泄漏的坑,也大概知道解决方法,然后没有重视起来。。。然后今天用CADisplayLink
自定义动画的时候,终于被坑了。。。痛定思痛决定好好梳理下相关知识
NSTimer,CADisplayLink造成循环引用
//CADisplayLink
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(sayHello)]
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
//NSTimer
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(sayHello) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
如上代码NSTimer
和CADisplayLink
都需要加入到NSRunloop
里面才能生效,NSRunloop
强引用了NSTimer
和CADisplayLink
对象,同时NSTimer
和CADisplayLink
对象又把self设置成了自己的target
,于是他们强引用了self
.因为self
一直被Runloop
强引用所以就释放不了,造成内存泄漏。
举个具体点的栗子
ContollerA
push了ControllerB
,但是ControllerB
里启动了一个NSTimer
,如果NSTimer
没有被释放,那么ControllerB
在被pop
的时候就不会被释放,早成了内存泄漏。
这里使用下Xcode8调试黑科技:Memory Graph来检测下内存泄漏:
ContollerB
push了两次之发现调试面板中SecondViewController
有两个实例对象
这两个对象的内存图例如下
好吧,感谢苹果爸爸的黑科技...以后不是瞎子的,都能看出内存泄漏了。第一张图显然是没有被释放的Controller也就是第一次push的那个Controller,从图中明显看出Runloop
引用这Timer
,Timer
引用着Controller
导致Controller
无法释放
解决方案
-
在对象
dealloc
之前使用invalidate
方法停止Timer,这样Timer
就会被释放。不会造成内存泄漏。但是如果我想让Timer
一直运行直到对象被dealloc
的时候才被停止,显然这个方法并不适用,因为如果不调用invalidate
方法,对象根本不会被销毁,deallco
方法根本不会执行 -
为了满足在对象销毁的时候停止定时器的需求,还有一种方案就是替换
target
,比较常见的是让NSTimer
类自己作为target
,配合block传递需要执行的方法。
@interface NSTimer (ZBBlockSupport)
+ (instancetype)zb_timerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)())timeBlock repeats:(BOOL)repeats;
@end
@implementation NSTimer (ZBBlockSupport)
+ (instancetype)zb_timerWithTimeInterval:(NSTimeInterval)timeInterval
block:(void (^)())timeBlock
repeats:(BOOL)repeats{
return [self timerWithTimeInterval:timeInterval
target:self
selector:@selector(zb_blockInvoke:)
userInfo:[timeBlock copy]
repeats:repeats
];
}
+ (void)zb_blockInvoke:(NSTimer *)timer{
void(^block)() = timer.userInfo;
if (block) {
block();
}
}
使用的时候需要注意block的循环应用问题,在闭包中使用self需要改为weak引用
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer zb_timerWithTimeInterval:1 block:^{
[weakSelf sayHello];
}
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
3.iOS10新的API- (void)timerWithTimeInterval: repeats: block:
支持了这种block的形式..看来苹果爸爸已经注意到NSTimer
这个坑了
if ([UIDevice currentDevice].systemVersion.floatValue == 10.0) {
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf sayHello];
}];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
同理CADisplayLink
也可以做类似的处理,避免内存泄漏。附上demo地址NSTimer
还有一些细节可能有疏漏,希望大家指正,或者有更好的实现方式欢迎讨论。附上博客地址