iOS从timer释放问题看内存管理
在iOS的开发中,如果使用NSTimer做定时器,一定要在合适的时机销毁这个定时器,不然可能导致内存得不到释放。原因就是循环引用。
举个例子:
我们新建一个工程,再创建一个新的OtherViewController:
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *Btn = [UIButton buttonWithType:UIButtonTypeCustom];
Btn.frame = CGRectMake(100, 400, 100, 40);
Btn.backgroundColor = [UIColor grayColor];
[Btn setTitle:@"跳转" forState:UIControlStateNormal];
[Btn addTarget:self action:@selector(Btn) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:Btn];
}
-(void)Btn{
OtherViewController *otherVC = [[OtherViewController alloc]init];
[self presentViewController:otherVC animated:YES completion:nil];
}
在OtherViewController里,我们构造一个定时器:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIButton *Btn = [UIButton buttonWithType:UIButtonTypeCustom];
Btn.frame = CGRectMake(100, 400, 100, 40);
Btn.backgroundColor = [UIColor grayColor];
[Btn setTitle:@"跳回" forState:UIControlStateNormal];
[Btn addTarget:self action:@selector(Btn) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:Btn];
[self addTimer];
}
-(void)addTimer{
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(logStr) userInfo:nil repeats:YES];
}
-(void)logStr{
NSLog(@"1");
}
-(void)Btn{
[self dismissViewControllerAnimated:YES completion:nil];
}
-(void)dealloc{
[timer invalidate];
timer = nil;
NSLog(@"dealloc");
}
当我们点击跳回按钮dissmiss的时候,dealloc方法并没有得到调用,timer还在一直跑着,因为dealloc方法的调用得在timer释放之后,而timer的释放在dealloc里,相互等待,这样就永远得不到释放了。所以这个timer释放时机不对。造成这种问题的根本原因是:
Timer 添加到 Runloop(这里是主线程,默认开启了runloop) 的时候,会被 Runloop 强引用,然后 Timer 又会有一个对 Target 的强引用(也就是 self ),循环引用了,也就是 NSTimer 强引用了 self ,导致 self 一直不能被释放掉,所以也就走不到 self 的 dealloc 里。
在平常情况下,一般我们都能给出正确的释放时机,而如果在写SDK这种就是需要控制器销毁时timer释放的需求时,由于SDK不能干预或是了解开发者会怎样操作,所以尽量自身把这些释放做好。
我们可以从循环引用这个点出发,打破循环引用,把target由self改为某个临时变量就行,举个例子:
我们新建一个类TheObject,继承于NSObject,在TheObject类里添加logStr这个方法
-(void)logStr{
NSLog(@"1");
}
然后在OtherViewController里把target由self变为TheObject的一个对象:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIButton *Btn = [UIButton buttonWithType:UIButtonTypeCustom];
Btn.frame = CGRectMake(100, 400, 100, 40);
Btn.backgroundColor = [UIColor grayColor];
[Btn setTitle:@"跳回" forState:UIControlStateNormal];
[Btn addTarget:self action:@selector(Btn) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:Btn];
obj = [[TheObject alloc]init];
[self addTimer];
}
-(void)addTimer{
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: obj selector:@selector(logStr) userInfo:nil repeats:YES];
}
-(void)Btn{
[self dismissViewControllerAnimated:YES completion:nil];
}
-(void)dealloc{
[timer invalidate];
timer = nil;
NSLog(@"dealloc");
}
这时运行,跳转OtherViewController,定时器也会调用,跳回的时候,dealloc方法也会走,定时器得到释放,停止输出。这其实是一种好的解决办法,本质在于打破循环引用。网上还有一些别的方法,本质上也是这样的。
另外,其实如果我们使用GCD的timer,我们就不用考虑这个问题:
@interface OtherViewController ()
{
dispatch_source_t GCD_timer;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIButton *Btn = [UIButton buttonWithType:UIButtonTypeCustom];
Btn.frame = CGRectMake(100, 400, 100, 40);
Btn.backgroundColor = [UIColor grayColor];
[Btn setTitle:@"跳回" forState:UIControlStateNormal];
[Btn addTarget:self action:@selector(Btn) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:Btn];
[self addTimer];
}
-(void)addTimer{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
GCD_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(GCD_timer, DISPATCH_TIME_NOW,
1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(GCD_timer, ^() {
NSLog(@"1");
});
dispatch_resume(GCD_timer);
}
-(void)Btn{
[self dismissViewControllerAnimated:YES completion:nil];
}
-(void)dealloc{
NSLog(@"dealloc");
}
我们没有调用GCD timer的释放方法
dispatch_source_cancel(GCD_timer);
dealloc方法还是走到了,这是因为GCD已经给我们做好了timer避免循环引用的机制。但我们使用GCD timer的时候还是要
注意:dispatch_suspend 状态下直接释放定时器,会导致定时器崩溃。
初始状态,挂起状态,都不能直接调用
dispatch_source_cancel(timer);
调用就会导致app闪退。
建议:使用懒加载创建定时器,并且记录当timer 处于dispatch_suspend的状态。这些时候,只要在 调用dealloc 时判断下,已经调用过 dispatch_suspend 则再调用下 dispatch_resume后再cancel,然后再释放timer。
如果暂停后不进行重新启动 timer 的话,直接取消 timer会报错。一旦取消timer后就不能再重新运行 timer,否则就会崩溃,只能重建一个new timer。
好的,从这个问题我们思考iOS的内存管理:
现在的iOS开发基本都是ARC的,ARC也是基于引用计数的,只是编译器在编译时期自动在已有代码中插入合适的内存管理代码(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些优化。,所以开发人员大部分情况都是不需要考虑内存管理的,因为编译器已经帮我们做了。这里为什么说是大部分,因为底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。如调用
CFRetain(<#CFTypeRef cf#>)
CFRelease(<#CFTypeRef cf#>)
还有就算循环引起情况就算由于互相之间强引用,引用计数永远不会减到0,所以需要自己主动断开循环引用,使引用计数能够减少。如上或常在block中使用的:
__weak 和 __block