今天闲了下来,记录一个特别棘手,又奇葩的问题,就是咱们常用的NSTimer计时器,我前几天在项目中遇到一个问题,就是NSTimer内存泄露等问题,我们很多人都喜欢在页面销毁的时候,把定时器在delloc中直接这样写
-(void)dealloc{
//停止定时器
[Timer invalidate];
Timer=nil;
}
甚至还有朋友这样写
-(void)dealloc{
//停止定时器
[Timer invalidate];
}
这样写就是错,大错特错,先说说按照这两种方法来写,你可以看看你的dealloc绝对没有走,dealloc不走,这可是一个非常严重的内存泄露的问题,也是引起闪退和内存越来越大的问题,那么咱们继续探究,我试着把timer去掉
-(void)dealloc{
}
这时我发现dealloc正常走了,是咱们写的停止NSTimer的代码有问题吗?
当然不是,因为scheduledTimerWithTimeInterval不仅仅是创建了NSTimer对象, 还把该对象加入到了当前的runloop中!
这和销毁NSTimer什么关系呢?我们先了解使用NSTimer时, ARC是怎么工作的?
首先, 是创建NSTimer, 加入到runloop后, 除了ViewController之外iOS系统也会强引用NSTimer对象,当调用invalidate方法时, 移除runloop后, iOS系统会解除对NSTimer对象的强引用, 当ViewController销毁时, ViewController和NSTimer就都可以释放了
当将NSTimer对象置nil时, 虽然解除了ViewController对NSTimer的强引用, 但是iOS系统仍然对NSTimer和ViewController存在着强引用关系
神马? iOS系统对NSTimer有强引用很好理解, 对ViewController本来不就是强引用么?
这里所说的iOS系统对ViewController的强引用, 不是指为了实现View显示的强引用, 而是指iOS为了实现NSTimer而对ViewController进行的额外强引用
综上所述, 销毁NSTimer的正确姿势应该是
-(void)dealloc{
//停止定时器
[Timer invalidate];// 真正销毁NSTimer对象的地方
Timer=nil;// 对象置nil是一种规范和习惯
}
对的,咱们的代码是没有错的,我解释了这么多就是为了证明这一点,那为什么不走dealloc方法呢?
别急,我们继续往下看
因为创建NSTimer的时候,他的计数为1,但是他被加到runloop中之后计数为2,说到这里有很多朋友大概都懂了,就算页面销毁NSTimer的时候,引用计数器也是为2,NSTimer不为nil,ios系统在ARC下,是不会重置内存的,所以这就导致了指向NSTImer内存的指针变成了野指针,最终内存泄露
所以我们要在其他生命周期方法里, 例如ViewWillDisappear中再次进行销毁,
-(void)viewDidDisappear:(BOOL)animated{
[superviewDidDisappear:animated];
if(Timer!=nil){
[Timer invalidate];
Timer=nil;
}
这样咱们才是真正的销毁了NSTimer,也不会有任何内存泄露的问题。
可惜好景不长,有碰到一个问题,比如说咱们的定时器是一个短信验证码倒计时,当咱们点击倒计时按钮后,NSTImer开始跳动,正常的逻辑是如果该页面pop到上个界面,再push该界面,NSTImer重置;
如果是该界面push到下个界面,然后再pop回该界面,那么定时器应该是持续跳动,不会停;
但是结果一试不是这样的,不管是pop上个界面,再push该界面,还是push下个界面,再pop回该界面,前者是NSTimer被重置了,后者是NSTImer跳动数字停止在了push下个界面时候的那个数字,怎么办?这不是我们想要的效果呀,其实不难解决
因为push下个界面,周期只走了-(void)viewDidDisappear:(BOOL)animated; 而没有走-(void)dealloc
而pop上个界面,周期不仅走了-(void)viewDidDisappear:(BOOL)animated; 同时也走了-(void)dealloc
为了实现咱们想要的效果,可以在-(void)viewDidDisappear:(BOOL)animated; 中添加一个判断方法,判断是pop上个界面,还是push下个界面,代码这样写
-(void)viewDidDisappear:(BOOL)animated{
[superviewDidDisappear:animated];
//判断页面是pop到上个界面还是push到下个界面
NSArray * viewControllers = self.navigationController.viewControllers;
if(viewControllers.count>1&& [viewControllers objectAtIndex:viewControllers.count-2] ==self){
//push到下个界面,不销毁也不停止NSTimer
}else{
//pop到上个界面,销毁停止NSTimer
if(Timer != nil){
[Timer invalidate];
Timer=nil;
}
}
}
这样就大功告成了,提醒大家一定要重视内存的问题,千里之堤毁于蚁穴,这不是小问题。
网友评论