在使用NSTimer 的过程中可能会遇到控制器明明销毁了,但是就是不会走控制器的dealloc 销毁方法,NSTimer也没有停止运行,这就说明在使用NSTimer过程中出现了循环引用导致的内存泄露问题 ,在stackoverflow 上看到了一种巧妙的方式解除这种循环引用,在此记录一下。
发生内存泄漏的场景 例如我们在viewController中声明了NSTimer属性,然后在viewDidLoad中去创建一个NSTimer实例赋值给myTimer,让它在dealloc方法中对myTimer进行销毁操作(我们只考虑在dealloc方法中对其销毁)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @interface ViewController () @property (nonatomic, weak) NSTimer *myTimer; @end - (void)dealloc { [_myTimer invalidate]; } - (void)viewDidLoad { [super viewDidLoad]; self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(logTime:) userInfo:@"打印log" repeats:YES]; } - (void)logTime:(NSTimer *)timer { NSLog(@"%@", timer.userInfo); }
这个viewController控制器是我从其他控制器push过来的,这时候我又pop到上一个页面,按道理说viewController应该在这个时候会走dealloc销毁方法对_myTimer进行销毁操作,然后并没有,结合上篇对NSTimer的介绍 ,分析原因如下:
当前线程RunLoop对NSTimer对象强引用
NSTimer又对self强引用
这样就造成了控制器(self)如果想要销毁的话,NSTimer就要先调用invalidate来销毁
但是控制器(self)不销毁的话
这就产生了一个死循环,谁也释放不掉
到这里会让我们理所当然地想到用__weak
来避免循环引用:
1 2 __weak typeof(self) weakSelf = self; self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(logTime:) userInfo:@"111" repeats:YES];
结果还是没有避免循环引用的问题,stackoverflow 的回答有这样一段话:
It won’t have the effect of creating an NSTimer with a weak reference. The only difference between that code and using a __strong reference is that if self is deallocated in between the two lines given then you’ll pass nil to the timer.
意思是说 weak 和 strong 唯一的区别就是:如果在这两行代码执行的期间 self 被释放了, NSTimer 的 target 会变成 nil。我们用下面这段代码来理解一下这里用__weak修饰是神马意思:
1 2 3 4 5 __weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ weakSelf.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(logTime:) userInfo:@"111" repeats:YES]; });
我们让NSTimer添加的代码延后2s执行,进入到这个控制器后立即(2s内)pop出控制器,然后在block中打断点打印self,这时候self是空的,NSTimer的target也就自然而然是nil。
解决办法 想解决这个问题,其实就是想办法让NSTimer不强引用self,那么我们可以创建一个中间对象来替代self,让NSTimer的target强引用这个中间对象,具体实现步骤如下:
定义一个LEOTimerTarget中间类,它的实例对象专门用来取代当前控制器(self)。
然后我们为LEOTimerTarget类定义target和selector属性,这两个属性是用来存放目标对象(这里是控制器)和要执行的目标方法(这里指控制器中的方法)。
然后在NSTimer重复执行任务的方法中判断LEOTimerTarget实例对象的target是否为空,为空的话说明目标对象(这里指当前控制器)已经销毁,接着让NSTimer实例调用invalidate
销毁,如果不为空,就通过performSelector:withObject:
让目标对象去执行目标方法。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @interface LEOTimerTarget : NSObject // 目标对象 @property (nonatomic,weak) id target; // 目标方法 @property (nonatomic,assign) SEL selector; // NSTimer实例 @property (nonatomic, weak) NSTimer *timer; @end @implementation LEOTimerTarget - (void)fire:(NSTimer *)timer { // target为弱引用,所以当target对象销毁时,target 为nil if (self.target) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.target performSelector:self.selector withObject:timer.userInfo]; #pragma clang diagnostic pop }else { [timer invalidate]; } } @end // 注意这里不是LEOTimerTarget类 @implementation LEOTimer + (NSTimer *)scheduleTimerWithTimerInterval:(NSTimeInterval)interval target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats { LEOTimerTarget *leoTimerTarget = [[LEOTimerTarget alloc]init]; leoTimerTarget.target = aTarget; leoTimerTarget.selector = aSelector; leoTimerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:leoTimerTarget selector:@selector(fire:) userInfo:userInfo repeats:repeats]; return leoTimerTarget.timer; } @end
这样,我们就避免了循环引用的问题,而且无需重复去写NSTimer的销毁方法invalidate
。
如果更习惯block调用方式的话,我们也可以在其基础上添加一个block调用接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + (NSTimer *)scheduleTimerWithTimerInterval:(NSTimeInterval)interval blockHandle:(LEOTimerBlockHandle)block userInfo:(id)userInfo repeats:(BOOL)repeats { // 这里的self指的是LEOTimer类,它调用的是类方法timerBlockInvoke: return [self scheduleTimerWithTimerInterval:interval target:self selector:@selector(timerBlockInvoke:) userInfo:@[[block copy],userInfo] repeats:repeats]; } + (void)timerBlockInvoke:(NSArray *)userInfo { LEOTimerBlockHandle block = userInfo[0]; id info = userInfo[1]; if (block) { block(info); } }
但是这种block调用方式需要我们在获取NSTimer实例对象后,在合适的位置通过调用invalidate
去销毁。
参考: