美文网首页
内存管理-ARC

内存管理-ARC

作者: Saxon_Geoffrey | 来源:发表于2015-03-02 15:08 被阅读404次

    理解引用计数机制

    在引用计数的机制下,每个对象都有一个计数器,可递增递减,用以表示当前有多少事物想让该对象继续保留下去。NSObject 协议声明了三个方法用于操作计数器:

    • retain:递增计数器
    • release:递减计数器
    • autorelease : "autorelease pool" 清理时,再递减计数器。

    retain 和 release

        NSNumber *number = @1;//number引用计数递增1
        [array addObject:number];////number引用计数递增1
    

    对象A被创建出来时,会自动执行 retain ,引用计数递增。当对象B引用了对象A时,被引用者对象A递增。当对象A的引用计数为0时,对象会被系统回收。

    NSMutableArray *array = [NSMutableArray array];//计数为1
    NSNumber *number = @1;//计数为1
    [array addObject:number];//计数为2
    [number release];//计数为1
    [array release];//0
    

    如上,如果我们不需要对象了,就对它调用release,然而,此时number的计数仍然为1,number对象仍然存活。为了避免不经意间使用了无效指针,调用完release我们可以手动清空指针。(通常都是针对局部变量)

    number = nil;
    

    我们知道,程序在生命期间会创建很多对象,这些对象都相互联系着。所以,这些相互联系的对象就构成一个对象树,这个对象树的根节点是UIApplocation对象,它是程序启动时创建的单例。

    autorelease

    - (NSString *)temp{
        NSString *tmp = @"A";//计数为1
        return tmp;
    }
    

    如上,返回的temp对象的引用计数比我们期望的要多1,因为只有retain,没有对应的release操作。然而,我们又不能在方法中释放temp,否则这个方法就没有存在的意义了。
    所以这里我们使用autorelease,它会在稍后释放对象,从而给调用者留下足够的时间,又不会因为没有释放而造成内存泄露。具体时间是当前线程的下一次时间循环。

    - (NSString *)temp{
        NSString *tmp = @"A";//计数为1
        return [tmp autorelease];
    }
    

    ARC

    为什么要使用ARC

    使用 ARC 时,引用计数还是会执行的,只不过是 ARC 为你自动添加的,即上面所说的retain/release/ autorelease 方法。在对象被创建时 retain count +1,在对象被 release 时 retain count -1.当 retain count 为0 时,销毁对象。 程序中加入 autoreleasepool 的对象会由系统自动加上autorelease方法,如果该对象引用计数为0,则销毁。 因为ARC会自动执行这些方法,所以在ARC下调用这些方法是非法的,会产生编译错误。

    那么ARC是为了解决什么问题诞生的呢?这个得追溯到MRC手动内存管理时代说起。
    MRC下内存管理的缺点:
    1.当我们要释放一个堆内存时,首先要确定指向这个堆空间的指针都被 release 了。(避免提前释放)
    2.释放指针指向的堆空间,首先要确定哪些指针指向同一个堆,这些指针只能释放一次。(MRC下即谁创建,谁释放,避免重复释放)
    3.模块化操作时,对象可能被多个模块创建和使用,不能确定最后由谁去释放。
    4.多线程操作时,不确定哪个线程最后使用完毕。

    内存管理语义

    ARC下,我们创建一个属性,需要主动声明一下内存管理语义:

    assign

    适用于基本数据类型。赋值特性,不涉及引用计数,弱引用,仅仅是基本数据类型变量(scalar type,例如 CGFloat 或 NSlnteger 等) 在 setter 方法中的简单赋值操作。assign 其实也可以用来修饰对象,那么我们为什么不用它呢?因为用了就相当于回到 MRC 时代了:)。 被assign修饰的对象在释放之后,指针的地址还是存在的,也就是说指针并没有被置为nil。如果在后续的内存分配中,刚好分到了这块地址,程序就会崩溃掉。

    strong

    强引用,与MRC中retain类似,使用之后,计数器+1。

    weak

    弱引用 ,weak 修饰的对象在释放之后,指针地址会被置为nil,可以有效的避免野指针,其引用计数为1。在 ARC 中,在有可能出现循环引用的时候, 此时通过引用计数无法释放指针, 往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性。自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。

    weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 。

    readwrite

    可读可写特性,需要生成getter方法和setter方法时使用。

    readonly

    只读特性,只会生成getter方法,不会生成setter方法,不希望属性在类外改变。

    copy

    表示拷贝特性,setter方法将传入对象复制一份,需要完全一份新的变量时。相当于strong,只不过在编译器生成的 setter 方法中会额外将传入对象复制一份。如果你自己实现了 setter 方法,不要忘了对变量进行 copy 操作,否则 copy 的声明就没有作用了。

    nonatomic

    非原子操作,不加同步,多线程访问可提高性能,但是线程不安全的。决定编译器生成的setter getter是否是原子操作。

    atomic

    原子操作,与nonatomic相反,系统会为setter方法加锁。 具体使用 @synchronized(self){//code } 。它是线程安全,需要消耗大量系统资源来为属性加锁 。(实际上并没有 atomic,只不过当没有 nonatomic 时就是 atomic,不过你要是写上去编译器也不会报错)。

    内存泄露

    即便有了ARC,也是有可能会内存泄露的。

    两个类循环引用

    //AViewController 对象
    @interface AViewController : UIViewController
    //aVC强引用了bVC
    @property (nonatomic, strong) BViewController *bVC;
    @end
    
    /**************************************************/
    
    //BViewController 对象
    @interface BViewController : UIViewController
    //bVC强引用了aVC
    @property (nonatomic, strong) aViewController *aVC;
    @end
    

    如上的代码,A强引用了B,B也强引用了A,这样就导致了循环引用,ARC无法回收这两个对象,从而导致内存泄露。

    那我们该怎么解决呢?我觉得两个对象最好不要互相引用,如果不得不互相引用,我们可以这么写:

    @interface AViewController : UIViewController
    //aVC强引用了bVC
    @property (nonatomic, strong) BViewController *bVC;
    @end
    
    /**************************************************/
    
    //BViewController 对象
    @interface BViewController : UIViewController
    //bVC弱引用了aVC
    @property (nonatomic, weak) aViewController *aVC;
    @end
    

    delegate循环引用问题

    delegate循环引用问题比较基础,原理和两个类循环引用一样,这里特地拿出来讲是因为 delegate 比较常用。具体可看下面的示例图。只需注意将代理属性修饰为weak即可。

    @property (nonatomic, weak) id delegate;
    

    Block

    有这样一个场景:在网络工具类NetworkFetch中有一个网络请求的回调Block:

    /**
     * 本例取自《Effective Objective-C 2.0》
     * NetworkFetecher 为自定义的网络获取器的类
     */ 
    //EOCNetworkFetcher.h
    #import <Foundation/Foundation.h>
    typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
    @interface EOCNetworkFetcher : NSObject
    @property (nonatomic, strong, readonly) NSURL *url;
    - (id)initWithURL:(NSURL *)url;
    - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
    @end;
    
    //EOCNetworkFetcher.m
    #import "EOCNetworkFetcher.h"
    @interface EOCNetworkFetcher ()
    @property (nonatomic, strong, readwrite) NSURL *url;
    @property (nonatomic, copy) (EOCNetworkFetcherCompletionHandler)completionHandler;
    @property (nonatomic, strong) NetworkFetecher *networkFetecher;
    @end;
    @implementation EOCNetworkFetcher
    - (id)initWithURL:(NSURL *)url{
        if (self = [super init]) {
            _url = url;
        }
        return self;
    }
    - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{
        self.completionHandler = completion;
    }
    - (void)p_requestCompleted{
        if (_completionHandler) {
            _completionHandler(_downloaderData);
        }
    }
    

    某个类中使用网络工具类发送请求并处理回调:

    @implementation EOCClass {
        EOCNetworkFetcher *_networkFetcher;
        NSData *_fetcherData;
    }
    - (void)downloadData{
        NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
        _networkFetcher = [[EOCNetworkFetch alloc] initWithURL:url];
        [_networkFetcher startWithCompletionHandler:^(NSData *data) {
            NSLog(@"request url %@ finished.", _networkFetcher);
            _fetcherData = data;
        }]
    }
    @end;
    

    很明显在使用block的过程中形成了循环引用:self 持有 networkFetecher;networkFetecher持有block;block持有 self。三者形成循环引用,内存泄露。

    下面的例子也会造成内存泄露:

    - (void)downloadData{
        NSURL *url = [[NSURL alloc] initWithString:@"/* some url string */"];
        NetworkFetecher *networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
        [networkFetecher startWithCompletionHandler:^(NSData *data){
            NSLog(@"request url: %@", networkFetcher.url);
        }];
    }
    

    networkFetecher持有block,block持有networkFetecher,形成内存孤岛,无法释放。

    解决方案有两种:

    将对象置为nil,消除引用,打破循环引用。这种方法容易漏掉某个该置nil的属性。
    代码中任意地方

    _networkFetecher = nil;
    

    将强引用转换成弱引用,打破循环引用。

    __weak __typeof(self) weakSelf = self;
    

    如果想防止 weakSelf 被释放,例如当 block 回调是在子线程,block 执行的时候,主线程可能在block 执行到一半的时候就将 self 置空,所以可以再次强引用一次:

    // 将强引用转换成弱引用,打破循环引用
    __weak __typeof(self) weakSelf = self;
    NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];
    _networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
    [_networkFetecher startWithCompletionHandler:^(NSData *data){
        //如果想防止 weakSelf 被释放,可以再次强引用
        __typeof(&*weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            //do something with strongSelf
        }
    }];
    

    strongself是为了防止内存提前释放。具体原理可以假设A界面push到B界面,B界面执行Block如下:

    self.title = @"B界面";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
             NSLog(@"%@", weakSelf.title);
            });
    };
    self.block();
    

    如果B界面10秒之后返回A界面会正常打印weakSelf.title为B界面
    但如果B界面10秒之内返回A界面则会打印null,因为10秒之内返回,B界面执行dealloc销毁,内存提前销毁,B界面对应的self不存在,因此也不可能执行关于self的事项。

    self.title = @"B界面";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
          __strong typeof(self) strongSelf = weakSelf;
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              NSLog(@"%@", strongSelf.title);
          });
    };
    self.block();
    

    而如果具有strongSelf,会使B界面所对应的self引用计数+1,即使10秒内返回A界面, B界面也不会立刻释放。并且strongSelf属于局部变量,存在与栈中,会随着Block的执行而销毁。
    总之strongSelf就是为了保证Block中的事件执行正确。

    performSelector

    performSelector 就是在运行时执行一个selector。

    - (id)performSelector:(SEL)selector;
    [object methodName];
    [object performSelector:@selector(methodName)];
    

    如果有以下的代码:

    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];
    

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

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

    [object performSelector:selector];
    

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

    NSNotificationcenter

    这个比较常见,如果你在viewWillAppear 中监听了一个NSNotificationcenter,记得在 viewWillDisappear 中调用 removeObserver。同样的,如果你在viewDidLoad监听了一个NSNotificationcenter,记得在 dealloc 中调用 removeObserver。

    不过,请注意,这个写法也是有存在隐患的,当你的类本来有内存泄漏的时候,你的类很可能不会调用dealloc 方法,所以你在 dealloc 中调用 removeObserver 可以说没什么卵用。

    又或者,当你使用以下方法进行控制器的切换时:

    + (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;
    

    viewWillDisappear 也不会调用,所以你想当然的想在viewWillDisappear 中调用 removeObserver 也没什么卵用。

    所以正确的方式是使用上面说的方法 removeObserver 后,打 log 留意removeObserver 是否被调用到了。

    NSTimer

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

    看下面的例子:

    #import "TestNSTimer.h"
     
    @interface TestNSTimer ()
    @property (nonatomic, strong) NSTimer *timer;
    @end
    @implementation TestNSTimer
     
    - (instancetype)init {
        if (self = [super init]) {
            _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeRefresh:) userInfo:nil repeats:YES];
        }
        return self;
    }
     
    - (void)timeRefresh:(NSTimer*)timer {
        NSLog(@"TimeRefresh...");
    }
     
    - (void)cleanTimer {
        [_timer invalidate];
        _timer = nil;
    }
     
    - (void)dealloc {
        [super dealloc];
        NSLog(@"销毁");
        [self cleanTimer];
    }
     
    @end
    

    为了防止内存泄漏,我们在delloc方法中调用cleanTimer。然而这并没有什么作用,因为TestNSTimer对象并没有正常释放,所以 delloc 无法执行,定时器仍然在无限的执行下去。当前类销毁执行dealloc的前提是定时器需要停止并滞空,而定时器停止并滞空的时机在当前类调用dealloc方法时,这样就造成了互相等待的场景,从而内存一直无法释放。

    所以,如何解决?

    如上的解决方案为将cleanTimer方法外漏,在外部调用,如viewDidDisappearviewWillDisappear中清空计时器即可。

    可是并不是特别优雅,要是其他开发者忘记调用cleanTimer,或者 viewDidDisappear或viewWillDisappear 没有调用到,这个类就会一直存在内存泄漏,然后定时器也不会停止。

    你可以参考这个链接使用这种取巧的方法[https://blog.callmewhy.com/2015/07/06/weak-timer-in-ios/]

    ��所以我还是比较推荐用dispatch的timer,无须考虑这些事情:

    @interface XXGCDTimer()
    {
        dispatch_source_t _timer;
    }
    @end
        
    @implementation XXGCDTimer
    -(void) startGCDTimer{
        NSTimeInterval period = 1.0; //设置时间间隔
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
         _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), period * NSEC_PER_SEC, 0); //每秒执行
        dispatch_source_set_event_handler(_timer, ^{
            //在这里执行事件
            NSLog(@"每秒执行test");
        });
        
        dispatch_resume(_timer);
    }
        
    -(void) pauseTimer{
        if(_timer){
            dispatch_suspend(_timer);
        }
    }
        
    -(void) resumeTimer{
        if(_timer){
            dispatch_resume(_timer);
        }
    }
        
    -(void) stopTimer{
        if(_timer){
            dispatch_source_cancel(_timer);
            _timer = nil;
        }
    }
    @end
    

    具体可以使用 YYKit 中封装的 YYTimer ,和上面使用的原理一样。

    非OC对象内存处理

    对于一些非OC对象,使用完毕后其内存仍需要我们手动释放。举个例子,比如常用的滤镜操作调节图片亮度:

    CIImage *beginImage = [[CIImage alloc]initWithImage:[UIImage imageNamed:@"yourname.jpg"]];
    CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
    [filter setValue:beginImage forKey:kCIInputImageKey];
    [filter setValue:[NSNumber numberWithFloat:.5] forKey:@"inputBrightness"];//亮度-1~1
    CIImage *outputImage = [filter outputImage];
    //GPU优化
    EAGLContext * eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    eaglContext.multiThreaded = YES;
    CIContext *context = [CIContext contextWithEAGLContext:eaglContext];
    [EAGLContext setCurrentContext:eaglContext];
     
    CGImageRef ref = [context createCGImage:outputImage fromRect:outputImage.extent];
    UIImage *endImg = [UIImage imageWithCGImage:ref];
    _imageView.image = endImg;
    CGImageRelease(ref);//非OC对象需要手动内存释放
    

    在如上代码中的CGImageRef类型变量非OC对象,其需要手动执行释放操作CGImageRelease(ref),否则会造成大量的内存泄漏导致程序崩溃。其他的对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free等都需要注意。

    代理未清空引起野指针

    iOS 的某些API,delegate 声明为assign的,貌似是历史遗留问题。这样就会引起野指针的问题,可能会引起一些莫名其妙的crash。当一个对象被回收时,对应的delegate实体也就被回收,但是delegate的指针确没有被nil,从而就变成了游荡的野指针了。所以在delloc方法中要将对应的assign代理设置为nil,如:

    - (void)dealloc{
        self.myTableView.delegate = nil;
        self.myTableView.dataSource = nil;
    }
    

    地图类处理

    若项目中使用地图相关类,一定要检测内存情况,因为地图是比较耗费App内存的,因此在根据文档实现某地图相关功能的同时,我们需要注意内存的正确释放,大体需要注意的有需在使用完毕时将地图、代理等滞空为nil,注意地图中标注(大头针)的复用,并且在使用完毕时清空标注数组等。

    - (void)clearMapView{
        self.mapView.delegate =nil;
        self.mapView.showsUserLocation = NO;
        [self.mapView removeAnnotations:self.annotations];
        [self.mapView removeOverlays:self.overlays];
        [self.mapView setCompassImage:nil];
    }
    

    大次数循环内存暴涨问题

    for (int i = 0; i < 100000; i++) {
            NSString *string = @"Abc";
            string = [string lowercaseString];
            string = [string stringByAppendingString:@"xyz"];
            NSLog(@"%@", string);
    }
    

    该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。

    for (int i = 0; i < 100000; i++) {
            @autoreleasepool {
                NSString *string = @"Abc";
                string = [string lowercaseString];
                string = [string stringByAppendingString:@"xyz"];
                NSLog(@"%@", string);
            }
    }
    

    检测内存泄漏

    借助Xcode自带的Instruments工具(选取真机测试)

    重写dealloc方法

    简单暴力的重写dealloc方法,加入断点或打印判断某类是否正常释放。

    dealloc 调用方式如下: 如果 a 持有对象 b ,b 持有c, c持有 d, 假设 a 是一个vc(其实只要是个对象都是一样的) 这时候 a.navigationxxxx popviewcon...... 这时候如果a “本身”没有内存泄漏,dealloc 回正常执行,
    但在执行dealloc的,a 会驱使b 释放,b如果没有泄漏会执行b 的delloc 然后b 在dealloc 执行完之前首先驱使 c 释放,c 如果没有泄漏,在c的dealloc 执行完之前会首先驱使d 释放。这是整个释放链。

    我们现在假设a 确实没内存泄漏,但是b有,则b 的delloc 不会执行,这样c、d的也不会执行,你就会看到,a的delloc 执行了,但是 它所持有的b, b持有的c, c 持有的d 都没有释放。

    因此 单纯以一个delloc 来确定我整个类释放了是不准确的,你要保证你这个对象所有所持有的对象(系统对象应该不与考虑,即使你考虑了 系统控件/对象造成的对象你也解决不了)都执行了delloc 方法,你才可以保证的说:没有内存泄漏了。

    相关文章

      网友评论

          本文标题:内存管理-ARC

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