前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS从timer释放问题看内存管理

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

作者头像
清墨
发布2019-04-01 11:17:01
1.6K0
发布2019-04-01 11:17:01
举报
文章被收录于专栏:清墨_iOS分享清墨_iOS分享

在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
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.03.28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档