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

2019-04-01 11:17:01 浏览数 (1)

在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

0 人点赞