iOS中内存管理漫谈

作者: 庸者的救赎 | 来源:发表于2016-01-12 23:46 被阅读1028次

    内存管理!MRC说一堆blabla(当然啦,各种release的痛想必记忆犹新),ARC说一堆blabla...

    多么老生常谈的问题,想必每个开发者在起初不止一次的被问起这个问题,但是一旦被问起,一般大家都会说起'黄金法则’,然后@property,blabla一大堆…

    大家都知道iOS的内存管理是通过引用计数来管理的,当引用计数为0的时候,对象会自动释放掉,在ARC环境下系统会“自动管理”对象的引用计数,然而事实上并非如此,我们常常遇到试图控制不能正常释放问题,block使用的时候内存泄露.

    好吧,多么痛才能领悟,今天咱们就聊聊这个蛋疼的问题,当然啦是在ARC环境下,分别从这么几个地方聊聊

    • @property — 几个内存管理相关的修饰符
    • self — 大家有没有想过,selfARC下到底什么类型的,strong?weak?
    • block — 这玩儿大家每天都在写,但是说起来他的内存管理方式,不知道有多少能说全面的

    Property

    Property想必大家都写过,当然不排除还有很多一直在使用_ivar的,当然啦他们之间的区别以及该用那个好咱们今天不聊,如有想了解的同学请移步这里.属性的内存管理语义有 以下几个:

    assign:适用于基本数据类型,其set方法只会进行简单的赋值操作NSInteger,CGFloat...

    /** 基本数据类型*/
    @property (assign, nonatomic) NSInteger age;
    

    strong:强引用类型,被定义了该类型的属性会被其所指对象持有,为这种属性设置新值的时候,set方法先保留新值,并释放掉旧值,然后再把新值赋值上去

    /** 适用于对象类型,并且被持有*/
    @property (strong, nonatomic) UIView *view;
    

    weak:爱恨交织的weak!弱引用类型,被定义了该类型的属性不会被持有,在set的时候,既不保留新值也不会释放旧值,和assign类似,不同的是在该属性所指对象被销毁之后,属性值也会被置为nil

    /** 弱引用类型,对象释放后置为nil*/
    @property (weak, nonatomic) id<SSDelegate> delegate;
    

    unsafe_unretained:这个跟assign就更像了,但不同的是它适用于对象类型.同样的不会被持有,但与weak不同是,所指对象被销毁之后不会把属性置为nil

    /** 弱引用类型,对象释放后不会置为nil*/
    @property (unsafe_unretained, nonatomic) UIView *view;
    

    copy:它与所指对象是持有关系,与strong类似,然而set方法并不会保留新值,而是将其拷贝一份.所以,当你声明一个非可变集合类型(collection type)的时候应该使用这个修饰符,不单单是NSString,同样的NSArray,NSDictionary,NSSet也应当使用它来修饰,copy可以保护非可变集合类型(collection type)的封装性,因为在其传递过程中有可能会指向一个可变类型(mutable type),此时如果不是copy,那么设置完属性之后,其值可能会在对象不知情的情况下被更改了.所以使用copy修饰符,保证set的时候copy一份不可变(immutable)类型的值,确保前后的一致性.

    /** 适用于集合类型(collection type)*/
    @property (copy, nonatomic) NSString *name;
    

    Self

    看到这个self估计很多人应该开始吐槽了,这SB,self有啥内存管理可言?需要管理么?

    哥负责任的告诉你,需要!需要!需要!!!

    行,你说需要就需要吧,哪里需要?Show me the code!!!

    也许你可能会看到过这样代码,然后一头雾水的就略过了...

    #import "SSYNetwork.h"
    
    - (void)startRequestData {
        SSYNetwork *strongSelf = self;
        [strongSelf.delegate finishedRequestData:strongSelf];
        if (strongSelf.successCompletionBlock) {
            strongSelf.successCompletionBlock(strongSelf);
        }
        [strongSelf clearCompletionBlock];
    } 
    

    我头一次看到的时候也是一头雾水,这是干嘛呢?写得看起来很高大.仔细分析后就能够发现,这段代码之所以增加一行strongSelf,是为了防止提前释放导致crash.

    这是一个请求网络数据的start方法,然而却在里面调用了finish方法,其实还没有等到start方法执行结束并返回的时候,self已经被finishedRequestData:方法释放,造成crash,简单的来说大致是这样的:

    - (void)clickAvatar {
        // self持有delegate
        [self.delegate clickAvatar]; // clickAvatar这个代理方法释放了self
        // 这个时候self成了野指针,然后就Boom💣
    }
    

    那么现在我们可以考虑一下了,在ARC里面self到底是个什么状态?为何会这样?

    其实ARC下,self既不是strong,也不是weak,而是被我们忽略的unsafe_unretained

    在我们调用方法的时候,ARC不会对selfretainrelease,self的生命周期由他的调用方法来决定

    所以self还是需要做一定的内存管理,不然一不小心就会💣(Boom)

    Block

    block其实是我们几乎每天都在用的东西,然而又让人讨厌的东西,之所以讨厌,是因为没有做好内存管理,产生循环引用导致内存泄露甚至是crash,下面咱们就唠唠这个烦人的玩意儿

    声明方式

    // 局部变量
    returnType (^blockName)(parameterTypes) = ^returnType(parameters){...}
    
    // 属性,作为属性的时候其修饰符一定是copy
    @property (nonatomic, copy) returnType (^blockName)(parameters);
    
    // 作为参数
    - (void)someMethodThatTakesABlock:(returnType (^)(parameters))blockName;
    
    // 方法调用是作为参数
    [someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
    
    // 宏定义
    typedef returnType (^TypeName)(parameterTypes);
    TypeName blockName = ^returnType(parameters) {...};
    

    循环引用

    在使用block的时候产生了循环引用是最让人头疼的,那么为什么会产生循环引用呢?先看看产生循环引用的代码长什么样,再分析分析why?

    // 两个对象间相互引用
    @property (nonatomic, copy) void(^block)(void);
    self.block = ^{
        [self doSomething];
    };
    
    // 多个对象间相互引用,多对象相互引用更难以发现
    ClassA* objA = [[ClassA alloc] init];
    self.block = ^{
        [self doSomething];
    };
    self.objA = objA;
    

    上面的这段代码中selfblock之间相互持有,在block里面调用的doSomething方法,使得self引用计数+1,这个过程中self是不会被释放,这就导致了循环引用.OK,那么问题来了,如何解决呢?

    其实,解决这个问题就在于如何让block不持有self,要解决这个问题,只需要使用__weak声明一个弱引用(weakSelf)就可以了,代码如下:

    @property (nonatomic, copy) void(^block)(void);
    
    // 弱引用
    __weak typeof(self) weakSelf = self; 
    
    self.block = ^{
        [weakSelf doSomething];
    };
    

    搞定,这样之后我们再调用dealloc方法,打上断点,就可以看出self已经被释放了

    Wow!💐💐💐💐💐

    不要天真的以为这样就万事大吉,就可以丢下鼠标等胜利了!

    如果我说,有时候即使你使用了__weak做了弱引用,也还是有crash的危险!

    咱不逗行么.png

    请看下面的代码:

    // 弱引用
    __weak typeof(self) weakSelf = self; 
    
    // 异步线程,这里的block是作为一个参数的
    dispatch_async(dispatch_get_main_queue(), ^{
        weakSelf.xx = xx;
    });
    

    让我们分析一下原因,将block作为参数传递给dispatch_async时,系统会将block拷贝到堆(heap)上,如果block中使用了self持有的对象,就会retain self,因为dispatch_async并不知道self会在什么时候被释放,所以为了确保系统调度执行block中的任务时self没有被意外释放掉,dispatch_async必须自己retain一次self,任务完成后在release self.但是这里使用了__weak,使得dispatch_async没有增加self的引用计数,这就导致了在调度执行block之前,self可能已经被释放掉了,然后就crash💣,控制台提示BAD_ACCESS

    好吧,算你对了,那你说该咋办?

    嘿嘿,既然没有retain,那就想办法retain呗?代码如下:

    开启装逼模式.jpg
    // 弱引用
    __weak typeof(self) weakSelf = self; 
    
    // 异步线程,这里的block是作为一个参数的
    dispatch_async(dispatch_get_main_queue(), ^{
        // 加强引用防止提前释放,并且做判断
        __strong typeof(weakSelf) strongSelf = self; 
        if (strongSelf) {
            strongSelf.xx = xx;
        }
    });
    

    然而这样写就好了么?还有没有更好的方法?程序员都是有追求的,我们是一群高逼格的从业者!

    再装逼试试.jpg

    好吧,我只是想说还有更优雅的方式来写下面两句话

    __weak __typeof(self)weakSelf = self;
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    
    别激动,开个小玩笑.jpg

    有使用过ReactiveCocoa的同学应该知道,其实我们可以使用影子变量@weakify/@strongify这种方式同样能解决,至于关于影子变量的详细介绍这里就不多说了,有兴趣的同学可以到这里看看,使用影子变量后,代码就会像下面这样:

    @weakify(self);
    dispatch_async(dispatch_get_main_queue(), ^{
        @strongify(self);
        if (!self) return;
        self.xx = xx; // 这里就可以放心的使用self,简单粗暴
    });
    

    OK,装逼结束!!!
    如有错误之处,欢迎讨论.

    相关文章

      网友评论

        本文标题:iOS中内存管理漫谈

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