美文网首页iOS内存管理
iOS从timer释放问题看内存管理

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

作者: _清墨 | 来源:发表于2019-03-28 13:37 被阅读72次

    在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
    

    相关文章

      网友评论

        本文标题:iOS从timer释放问题看内存管理

        本文链接:https://www.haomeiwen.com/subject/wklqbqtx.html