美文网首页知识点
ARC下内存泄露总结

ARC下内存泄露总结

作者: Clang的技术博客 | 来源:发表于2016-06-16 15:37 被阅读788次

    1、Block的循环引用

      在iOS4.2时,Apple推出ARC内存管理机制。这是一种编译期的内存管理方式,在编译期间,编译器会判断对象的引用情况,并在合适的位置加上retain和release,使得对象的内存被合理的管理。所以,从本质上说ARC和MRC在本质上是一样的,都是通过引用计数的内存管理方式。
      使用ARC虽然可以简化内存管理,但是ARC并不是万能的,有些情况程序为了能够正常运行,会隐式地持有或者复制对象,如果不加以注意,便会造成内存泄露。在ARC下,当Block获取到外部变量时,由于编译器无法预测获取到的变量何时会被突然释放,为了保证程序能够正确运行,让Block持有获取到的变量。
      下面主要通过一个例子来介绍在ARC情况下使用Block不当会导致内存泄露的问题。示例代码来源于《Effective Objective-C 2.0》(编写高质量iOS与OS X代码的52个有效方法)。
    (1)EOCNetworkFetcher.h

    typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
    
    @interface EOCNetworkFetcher : NSObject
    
    @property (nonatomic, strong, readonly) NSURL *url;
    
    - (id)initWithURL:(NSURL *)url;
    
    - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
    
    @end
    

    (2)EOCNetworkFetcher.m

    @interface EOCNetworkFetcher ()
    
    @property (nonatomic, strong, readwrite) NSURL *url;
    @property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
    @property (nonatomic, strong) NSData *downloadData;
    
    @end
    
    @implementation EOCNetworkFetcher
    
    - (id)initWithURL:(NSURL *)url {
        if(self = [super init]) {
            _url = url;
        }
        return self;
    }
    
    - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {
        self.completionHandler = completion;
        //开始网络请求
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            _downloadData = [[NSData alloc] initWithContentsOfURL:_url];
            dispatch_async(dispatch_get_main_queue(), ^{
                 //网络请求完成
                [self p_requestCompleted];
            });
        });
    }
    
    - (void)p_requestCompleted {
        if(_completionHandler) {
            _completionHandler(_downloadData);
        }
    }
    
    @end
    

    (3)EOCClass.m

    @implementation EOCClass {
        EOCNetworkFetcher *_networkFetcher;
        NSData *_fetchedData;
    }
    
    - (void)downloadData {
        NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
        _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
        [_networkFetcher startWithCompletionHandler:^(NSData *data) {
            _fetchedData = data;
        }];
    }
    
    @end
    

    代码分析:

    • completion handler块因为要设置_fetchedData实例变量的值,所以它必须捕获self变量,也就是说handler块保留了EOCClass实例。
    • 而EOCClass实例通过strong实例变量保留了EOCNetworkFetcher,最后EOCNetworkFetcher实例对象又保留了handler块。

    引用关系如下下图所示


    循环引用示意图

    要想打破保留环,解决办法:

    • 方法一:使用完EOCNetworkFetcher对象之后就没有必要在保留该对象了,在block里面将对象释放即可打破保留环。
    - (void)downloadData {
        NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
        _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
        [_networkFetcher startWithCompletionHandler:^(NSData *data) {
            _fetchedData = data;
            _networkFetcher = nil;   //加上此行,此处是为了打破循环引用
        }];
    }
    
    • 方法二:上面的方法需要调用者自己来将对象手动设置为nil,对于使用者来说会造成很多困恼,如果忘记将对象设置为nil就会造成循环引用。在运行完completion handler之后将block释放即可。
    - (void)p_requestCompleted {
        if(_completionHandler) {
            _completionHandler(_downloadData);
        }
        self.completionHandler = nil;   //加上此行,此处是为了打破循环引用
    }
    
    • 方法三:将引用的一方变成weak,从而避免循环引用。
    - (void)downloadData {
       __weak __typeof(self) weakSelf = self;
       NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
       _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
       [_networkFetcher startWithCompletionHandler:^(NSData *data) {
            //如果想防止weakSelf被释放,可以再次强引用
            __typeof(&*weakSelf) strongSelf = weakSelf;
            if (strongSelf) {
                strongSelf.fetchedData = data;
            }
       }];
    }
    

    2、NSTimer

    在使用NSTimer addTarget时,为了防止target被释放而导致的程序异常,timer会持有target,所以这也是一处内存泄露的隐患。

    /**
     * self持有timer,timer在初始化时持有self,造成循环引用。
     * 解决的方法就是使用invalidate方法销掉timer。
     */
    @interface SomeViewController : UIViewController
    @property (nonatomic, strong) NSTimer *timer;
    @end
    @implementation SomeViewController
    
    - (void)someMethod
    {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.1  
                                                 target:self  
                                               selector:@selector(handleTimer:)  
                                               userInfo:nil  
                                                repeats:YES];  
    }
    
    @end
    

    3、performSelector 系列

    performSelector顾名思义即在运行时执行一个selector,最简单的方法如下

    - (id)performSelector:(SEL)selector;
    

    这种调用selector的方法和直接调用selector基本等效,执行效果相同

    [object methodName];
    [object performSelector:@selector(methodName)];
    

    但performSelector相比直接调用更加灵活

    SEL selector;
    if (/* some condition */) {
        selector = @selector(newObject);
    } else if (/* some other condition */) {
        selector = @selector(copy);
    } else {
        selector = @selector(someProperty);
    }
    id ret = [object performSelector:selector];
    

    这段代码就相当于在动态之上再动态绑定。在ARC下编译这段代码,编译器会发出警告

    warning: performSelector may cause a leak because its selector is unknow [-Warc-performSelector-leak]
    

    正是由于动态,编译器不知道即将调用的selector是什么,不了解方法签名和返回值,甚至是否有返回值都不懂,所以编译器无法用ARC的内存管理规则来判断返回值是否应该释放。因此,ARC采用了比较谨慎的做法,不添加释放操作,即在方法返回对象时就可能将其持有,从而可能导致内存泄露。

    以本段代码为例,前两种情况(newObject, copy)都需要再次释放,而第三种情况不需要。这种泄露隐藏得如此之深,以至于使用static analyzer都很难检测到。如果把代码的最后一行改成

    [object performSelector:selector];
    

    不创建一个返回值变量测试分析,简直难以想象这里居然会出现内存问题。所以如果你使用的selector有返回值,一定要处理掉。

    4、循环引用

    A有个属性B,B有个属性A,如果都是strong修饰的话,两个对象都无法释放。
    这种问题常发生于把delegate声明为strong属性了。

    @interface SampleViewController
    @property (nonatomic, strong) SampleClass *sampleClass;
    @end
    @interface SampleClass
    @property (nonatomic, strong) SampleViewController *delegate;
    @end
    

    5、循环未结束

    如果某个ViewController中有无限循环,也会导致即使ViewController对应的view关掉了,ViewController也不能被释放。
    这种问题常发生于animation处理。

    CATransition *transition = [CATransition animation];
    transition.duration = 0.5;
    tansition.repeatCount = HUGE_VALL;
    [self.view.layer addAnimation:transition forKey:"myAnimation"];
    

    上例中,animation重复次数设成HUGE_VALL,一个很大的数值,基本上等于无限循环了。
    解决办法是,在ViewController关掉的时候,停止这个animation。

    -(void)viewWillDisappear:(BOOL)animated {
        [self.view.layer removeAllAnimations];
    }
    

    6、非OBJC对象

    ARC是自动检测objc对象的,非objc对象就无能为力了,比如C或C++等。
    C语言使用malloc开辟,free释放。
    C++使用new开辟,delete释放。
    但是在ARC下,不会添加非objc对象释放语句,如果没去释放,也会造成内存泄露。

    Clang的技术博客:https://chenhu1001.github.io

    相关文章

      网友评论

      • f23387aa9467:现在也遇到了VC不释放的情况,ARC下dismiss页面时不走dealloc方法,但是我已经把内部的block、代理、计时器全部整理一遍,还是会有强引用存在,反而在再次present页面的时候调用dealloc,有啥建议不?还有什么可能造成的原因。
        Clang的技术博客:采用注释部分代码,一步一步定位

      本文标题:ARC下内存泄露总结

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