美文网首页
这篇文章让你搞懂iOS的MRC机制

这篇文章让你搞懂iOS的MRC机制

作者: 小心韩国人 | 来源:发表于2019-12-18 22:27 被阅读0次

    在iOS5之前,iOS开发者需要自己管理对象的内存,很是繁琐.直到ARC出来后这些工作才由编译器和Objective-C运行时库共同完成.
    虽然现在已经是ARC环境,内存管理相关的代码已经不需要开发者手动去写了,但是ARC的底层依然是MRC那一套,只不过这些手动管理内存的代码由编译器帮我们写了而已.所以要想更加深刻的理解OC的内存管理机制,就很有必要搞清楚MRC环境下的内存管理.

    • 在MRC环境下,当调用alloc , new , copy , mutableCopy返回的对象在不需要这个对象时,需要调用release 或者 autorelease来释放它.
      比如下面这样:
     Person *person = [[Person alloc]init];
    //不用的时候要记得销毁
    [person release];
    

    或者调动autorelease:

     Person *person = [[[Person alloc]init]autorelease];
    

    如果调用autorelease,那么对象就会在适当的时候自动调用 release释放,比如说:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
        Person *person = [[[Person alloc]init]autorelease];  
        }//执行大括号结束的的时候释放 [person release]
        return 0;
    }
    

    如果该释放的对象没有释放就会造成内存泄漏.
    下面代码运行会发生什么:

    @interface Person : NSObject
    {
        Dog *_dog;
    }
    - (void)setDog:(Dog *)dog;
    - (Dog *)dog;
    @end
    
    
    
    @implementation Person
    
    - (void)setDog:(Dog *)dog{
        _dog = dog;
    }
    
    - (Dog *)dog{
        return _dog;
    }
    
    - (void)dealloc{
        [super dealloc];
        NSLog(@"%s",__func__);
    }
    
    @end
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Dog *dog = [[Dog alloc]init];
            Person *person = [[Person alloc]init];
            [person setDog:dog];
            [dog release];
            
            [[person dog] run];
            //不用的时候要记得销毁
            [person release];
        }//如果调用 autorelease
        return 0;
    }
    

    上面代码一运行就出现了坏内存访问,dog已经release了,所以我们在执行[[person dog] run];的时候就崩溃了.但是这样显然不符合我们的要求,因为person还没有销毁,正常来讲,我们调用person.dog的方法应该是没有问题的.我们要做到如果person还在,dog也要保证存在.person要对dog保持持有关系.那应该怎么做呢?
    只需在setter方法中对dog对象retain即可:

    - (void)setDog:(Dog *)dog{
        _dog = [dog retain];
    }
    
    - (Dog *)dog{
        return _dog;
    }
    
    - (void)dealloc{
        //person对象一旦释放,就要放开对dog的持有关系
        [_dog release];
        _dog = nil;
        [super dealloc];
        NSLog(@"%s",__func__);
    }
    

    即使我们调用多遍也没有问题:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Dog *dog = [[Dog alloc]init];// dog 引用计数 1
            Person *person = [[Person alloc]init];
            [person setDog:dog];// dog 引用计数 2
            [dog release];// dog 引用计数 1
            
            
            Person *person2 = [[Person alloc]init];
            [person2 setDog:dog];// dog 引用计数 2
            
            [[person dog] run];
            //不用的时候要记得销毁
            [person release];// dog 引用计数 1
            [[person2 dog]run];
            [person2 release];// dog 引用计数 0
        }//如果调用 autorelease
        return 0;
    }
    
    

    这样还是会出现问题,比如像下面这样,Dog对象多次赋值给Person:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Dog *dog = [[Dog alloc]init];// dog 引用计数 1
            Person *person = [[Person alloc]init];
            [person setDog:dog];// dog 引用计数 2
            
            
            Dog *dog2 = [[Dog alloc]init];// dog2 引用计数 1
            [person setDog:dog2];//// dog2 引用计数 2
            
            [dog release];// dog 引用计数 1
            [dog2 release]; //dog2 引用计数 1
            // person 最后释放的引用计数是最后一个赋值给它的,也就是 dog2
            //所以dog2最后成功释放了,但是dog1还没有释放
            [person release];//dog2 引用计数 0
            
        }//如果调用 autorelease
        return 0;
    }
    

    上述代码的结果就是dog2会释放,但是dog不会被释放,这样就造成了内存泄漏.解决的办法就是每次再给Person内部的_dog赋值之前,都要先把旧对象释放掉:

    - (void)setDog:(Dog *)dog{
        [_dog release];
        _dog = [dog retain];
    }
    

    这样虽然不会报错了,但是还是会有别的问题,比如下面这样,重复给_dog赋值:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Dog *dog = [[Dog alloc]init]; //dog 引用计数  1
            Person *person = [[Person alloc]init];
            [person setDog:dog];//dog 引用计数  2
            [dog release];//执行这一行时,dog 引用计数 1
            [person setDog:dog];
            [person setDog:dog];    
            [person release];
            
        }//如果调用 autorelease
        return 0;
    }
    
    

    一运行一样报坏内存访问.这是因为[dog release]会对dog的引用计数减1,因为此时person内部还引用了dog,person此时也还没有释放,所以此时dog的引用计数还是1.再执行[person setDog:dog]时,在setter方法内部会先调用[_dog release];.再调用_dog = [dog retain];.而[_dog release]执行完毕后dog已经释放了.此时我们再去dog retain就报坏内存访问了.已经释放的内存,再去retain肯定不可以.那我们怎么解决呢?
    解决办法就是在setter方法内部判断一下,如果_dog和传进来的dog相同,就不进行任何操作:

    - (void)setDog:(Dog *)dog{
        if (_dog != dog) {
            [_dog release];
            _dog = [dog retain];
        }
    }
    

    这样我们就解决了这个问题,并且dealloc也可以直接写成self.dog = nil;

    - (void)dealloc{
        //person对象一旦释放,就要放开对dog的持有关系
        self.dog = nil;
        [super dealloc];
        NSLog(@"%s",__func__);
    }
    

    self.dog = nil就好比给setter方法直接传入nil:

    - (void)setDog:(Dog *)nil{
        if (_dog != nil) {
            [_dog release];
    //        _dog = [nil retain];   [nil retain]还是nil
            _dog = nil;
        }
    }
    

    @synthesize关键字

    随着时间的推移,xcode越来越智能化.比如我们这样写@property (nonatomic,assign)int age;,Xcode会自动给我们生成setter,getter方法的声明.@synthesize会自动生成下划线开头的成员变量和setter,getter方法的实现:

    //自动生成下划线开头的成员变量和setter,getter方法的实现
    @synthesize age = _age123;
    
    
    - (void)setAge:(int)age{
        _age123 = age;
    }
    
    

    再到后来连@ synthesize都不用写,@Property会自动生成成员变量,setter,getter方法的声明和实现.并且如果发现是assign修饰就直接赋值,没有任何内存管理相关的操作.发现是retain就生成内存管理相关的代码:

    - (void)setDog:(Dog *)dog{
        if (_dog != dog) {
            [_dog release];
            _dog = [dog retain]; //如果是 copy 这里就是 copy
        }
    }
    

    虽然Xcode越来越智能,但是在MRC年代还是需要在dealloc手动release的.

    • 小提示:在MRC环境下一些类方法开头的初始化方法不需要调用release或者autorelease,因为它的内部已经调用过release,比如说NSArray:
    + (instancetype)array{
        return [[[self alloc]init]autorelease];
    }
    

    copy 关键字

    我们在创建属性的时候有时候会用到copy关键字.copy的目的就是:产生一个副本对象跟源对象互不影响.也就是修改了源对象不会影响副本对象;修改了副本对象也不会影响源对象.
    iOS中提供了两个copy方法:
    1: copy:不可变拷贝,产生不可变副本
    2: mutableCopy:可变拷贝,产生可变副本.
    举例:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            NSString *str = [NSString stringWithFormat:@"123"];
            NSMutableString *str1 = [str copy];
            NSMutableString *str2 = [str mutableCopy];
    //        [str1 appendString:@"456"]; //报错
            [str2 appendString:@"456"];
            NSLog(@"%@,%@",str1,str2);
            
        }//如果调用 autorelease
        return 0;
    }
    
    结果:
    2019-12-18 21:28:42.476911+0800 OC内存管理MRC[1728:970673] 123,123456
    

    copymutableCopy有没有什么区别呢?

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSString *str1 = [NSString stringWithFormat:@"123"];
            NSMutableString *str2 = [str1 copy];
            NSMutableString *str3 = [str1 mutableCopy];
            
            NSLog(@"str1:%p,str2:%p,str3:%p",str1,str2,str3);
            
        }//如果调用 autorelease
        return 0;
    }
    
    结果:
    OC内存管理MRC[1768:976040] str1:0x9a20e77157d44f17,str2:0x9a20e77157d44f17,str3:0x100706070
    

    从打印结果可以看到str1str2的内存地址是一样的,str3的内存地址不一样.它们的内存关系如下:

    内存关系

    为什么会这样呢?大家想想copy的目的是什么?就是产生一个副本对象跟原对象互不影响.我们只要达到这个目的就行了.既然str1是不可变字符串,那我们就不必再开辟一块内存存放一个副本,因为它本身就是不可变的,我们只需对它retain一份即可,何必再浪费内存空间呢.而mutableCopy是产生一个可变对象,所以需要开辟一块新的内存存放可变str2.让str2的修改不影响到str1.
    我们对[str1 copy];进行copy操作其实就相当于retain:

    copy
    我们得出结论不可变对象调用 copy 返回的是他本身,只是相当于引用计数加1;可变对象调用 copy , 返回的是一个新的可变对象由此我们就引申出两个概念:深拷贝,浅拷贝
    深拷贝:内容拷贝,产生新对象
    浅拷贝:指针拷贝,没有产生新对象
    练习一下,下面的代码是深拷贝还是浅拷贝?
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableString *str1 = [NSMutableString stringWithFormat:@"他日若遂凌云志"];
            
            NSMutableString *str2 = [str1 copy];
            
            NSMutableString *str3 = [str1 mutableCopy];
            
            NSLog(@"str1:%p,str2:%p,str3:%p",str1,str2,str3);
            
        }//如果调用 autorelease
        return 0;
    }
    打印结果:
    OC内存管理MRC[1809:987734] str1:0x103009860,str2:0x100705870,str3:0x1007859a0
    
    

    答案全部都是深拷贝,原因如图:

    内存图
    NSArray,NSMutableArray,NSDictionary,NSMutableDictionary都和NSString,NSMutableString同样原理.我就不一一举例了,现在我们总结一下深拷贝,浅拷贝规则:
    拷贝规则

    练习一:

    下面代码的运行结果:

    @interface Person : NSObject
    @property (nonatomic,copy)NSMutableArray *mutableArray;
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            Person *person = [[Person alloc]init];
            person.mutableArray = [NSMutableArray array];
            [person.mutableArray addObject:@"a"];
        }//如果调用 autorelease
        return 0;
    }
    

    已运行就会发现报错:

    找不到方法
    为什么会这样呢?因为我们的mutableArray使用copy修饰的,所以我们在给它赋值的时候就像下面这样:
    - (void)setMutableArray:(NSMutableArray *)mutableArray{
        if (_mutableArray != mutableArray) {
            [_mutableArray release];
            _mutableArray = [mutableArray copy];
        }
    }
    

    调用[mutableArray copy]返回的是一个不可变数组,既然是一个不可变数组肯定没有addObject方法,就会报错.所以我们以后在项目中可变类型的对象不要用copy修饰,因为用copy修饰会得到一个不可变的对象.

    小提示:NSString一般都用copy修饰,因为用copy修饰就能保证肯定是个不可变字符串.要是想改变这个字符串就直接赋值一个新的字符串就好了.避免在外面使用appendString修改字符串影响到其他地方.

    自定义对象的copy

    mutableCopy主要是给Foundation框架的NSMutableString,NSMutableArray,NSMutableDictionary提供的.所以自定义对象只要考虑好copy就可以了.
    我们试一下自定义对象的copy:

    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person1 = [[Person alloc]init];
            Person *person2 = [person1 copy];
            
        }//如果调用 autorelease
        return 0;
    }
    

    直接报错:

    copyWithZone
    自定义的类要想实现copy操作,需要实现<NSCopying>协议的copyWithZone方法:
    - (id)copyWithZone:(NSZone *)zone{
        Person *person = [[Person alloc]init];
        person.dog = self.dog;
        return person;
    }
    

    相关文章

      网友评论

          本文标题:这篇文章让你搞懂iOS的MRC机制

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