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

2019-12-18  本文已影响0人  小心韩国人

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

 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的.

+ (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;
}
上一篇 下一篇

猜你喜欢

热点阅读