iOS从timer释放问题看内存管理

在iOS的开发中,如果使用NSTimer做定时器,一定要在合适的时机销毁这个定时器,不然可能导致内存得不到释放。原因就是循环引用。

举个例子:

我们新建一个工程,再创建一个新的OtherViewController:

代码语言:javascript复制- (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里,我们构造一个定时器:

代码语言:javascript复制- (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这个方法

代码语言:javascript复制-(void)logStr{

NSLog(@"1");

}然后在OtherViewController里把target由self变为TheObject的一个对象:

代码语言:javascript复制- (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,我们就不用考虑这个问题:

代码语言:javascript复制@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的释放方法

代码语言:javascript复制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 的管理下,所以需要自己维护这些对象的引用计数。如调用

代码语言:javascript复制CFRetain(<#CFTypeRef cf#>)

CFRelease(<#CFTypeRef cf#>)还有就算循环引起情况就算由于互相之间强引用,引用计数永远不会减到0,所以需要自己主动断开循环引用,使引用计数能够减少。如上或常在block中使用的:

代码语言:javascript复制__weak 和 __block