美文网首页
iOS-内存管理3-MRC

iOS-内存管理3-MRC

作者: Imkata | 来源:发表于2019-12-24 16:51 被阅读0次

    一. 初识MRC

    1. Automatic Reference Counting:ARC
      Manual Reference Counting:MRC
    2. 在iOS中,使用引用计数来管理OC对象的内存
    3. 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
    4. 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
    5. 内存管理的经验总结:
      ① 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
      ② 想拥有某个对象,就让它的引用计数+1,不想再拥有某个对象,就让它的引用计数-1
    6. 可以通过以下私有函数来查看自动释放池的情况
      extern void _objc_autoreleasePoolPrint(void);

    首先搜索“Automatic Reference Counting”,关闭ARC,将项目改成手动内存管理。

    //new方式和alloc init方式一样,只不过我们习惯了分两步走。
    //MJPerson *person = [MJPerson new];
    MJPerson *person = [[MJPerson alloc] init]; //1
    //中间写我们想要的代码
    [person release]; // 0
    
    或者:
    @autoreleasepool {
    MJPerson *person = [[[MJPerson alloc] init] autorelease]; //1
    //中间写我们想要的代码
    }
    

    我们可以手动进行release,也可以使用autoreleasepool,如果一个对象调用了autorelease,那么在@autoreleasepool{}结束之后,会对{}内部调用过autorelease的对象进行一次release操作。

    二. 逐步完善MRC的setter方法

    MJPerson里面拥有MJDog,在MJPerson里面重写setDog方法。

    MRC的标准setter方法如下,一步都不能少,下面解释为什么这么写?

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

    1. 为什么要retain新值

    当setDog方法的实现是如下这样:

    - (void)setDog:(MJDog *)dog
    {
        _dog = dog
    }
    

    执行以下代码:

    void test2()
    {
        MJDog *dog = [[MJDog alloc] init]; // dog:1
        
        MJPerson *person = [[MJPerson alloc] init]; // person:1
        [person setDog:dog]; // dog:1
        
        [dog release];  // dog:0
        
        [[person dog] run]; //报错
        
        [person release];
    }
    

    报错:

    -[MJDog run]: message sent to deallocated instance 0x10064dd40 
    

    意思是run消息发送给一个已经释放掉的对象了(僵尸对象)。

    上面代码,当[[person dog] run]的时候person还存在,但是dog却死了,这样肯定不合理啊,所以我们要让person拥有dog,所以给dog先retain一次再赋值。

    setter方法修改如下:

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

    基于谁使用谁负责的原则,既然person拥有了dog,那么在person死掉的时候也要把dog再release一次,所以person的dealloc方法要这么写:

    - (void)dealloc
    {
        [_dog release];
        _dog = nil;
    
        NSLog(@"%s", __func__);
        
        // 父类的dealloc放到最后,一般都是先释放子类对象再释放父类对象
        [super dealloc];
    }
    

    运行如下代码:

    void test2()
    {
        MJDog *dog = [[MJDog alloc] init]; // dog:1
    
        MJPerson *person1 = [[MJPerson alloc] init]; // person1:1
        [person1 setDog:dog]; // dog:2
        
        MJPerson *person2 = [[MJPerson alloc] init];  // person2:1
        [person2 setDog:dog]; // dog:3
    
        [dog release]; // dog:2
    
        [person1 release]; // person1:0  dog:1
    
        [[person2 dog] run];
        [person2 release];   // person2:0 dog:0
    }
    

    打印:

    -[MJPerson dealloc]
    -[MJDog run]
    -[MJDog dealloc]
    -[MJPerson dealloc]
    

    根据引用计数器分析和打印可知,两个person和一个dog都被释放了。

    达到的效果:只要有人使用狗,狗就不会销毁,这也是我们想要的效果。

    2. 为什么要release旧值

    执行以下代码:

    void test3()
    {
        MJDog *dog1 = [[MJDog alloc] init]; // dog1 : 1
        MJDog *dog2 = [[MJDog alloc] init]; // dog2 : 1
        
        MJPerson *person = [[MJPerson alloc] init]; // person : 1
        [person setDog:dog1]; // dog1 : 2
        [person setDog:dog2]; // dog2 : 2
        
        [dog1 release]; // dog1 : 1
        [dog2 release]; // dog2 : 1
        [person release]; //person : 0 dog2 : 0  对最后传进来的dog2进行release
    }
    

    打印:

    -[MJDog dealloc]
    -[MJPerson dealloc]
    

    可以发现,有一条狗没被释放,根据上面注释的引用计数器分析,可以知道是dog1没被释放,这样就造成了内存泄漏(该释放的对象没有释放)。

    因为dog1是旧值,所以赋新值之前要对旧值进行release操作,setter方法修改如下:

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

    重新执行以下代码:

    void test3()
    {
        MJDog *dog1 = [[MJDog alloc] init]; // dog1 : 1
        MJDog *dog2 = [[MJDog alloc] init]; // dog2 : 1
    
        MJPerson *person = [[MJPerson alloc] init]; // person : 1
        [person setDog:dog1]; // dog1 : 2
        [person setDog:dog2]; // dog2 : 2, dog1 : 1
    
        [dog1 release]; // dog1 : 0
        [dog2 release]; // dog2 : 1
        [person release]; //person : 0 dog2 : 0
    }
    

    调用 [person setDog:dog2] 的时候把dog1先release一次,最后person和两个dog都被释放了,也可参考上面的引用计数器分析进行理解。

    3. 为什么要判断新值旧值是否相等

    首先,打开僵尸对象检测,Edit scheme -> Run -> Diagnostics,勾选Zombie Objects。

    执行以下代码:

    void test4()
    {
        MJDog *dog = [[MJDog alloc] init]; // dog:1
        
        MJPerson *person = [[MJPerson alloc] init]; // person : 1
        [person setDog:dog]; // dog:2
        
        [dog release]; // dog:1
        
        [person setDog:dog]; // dog 0 
        [person setDog:dog];
        [person setDog:dog];
        
        [person release]; 
    

    报错:

    -[MJDog retain]: message sent to deallocated instance 0x10058cdf0
    

    错误意思就是,retain消息发送给一个已经释放掉的对象了(僵尸对象)。

    上面代码,执行[person setDog:dog]就是调用setter方法:

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

    先release旧值后retain新值,旧值和新值是一样的,由于旧值release之后引用计数器就为0,dog被释放了,这时候再[dog retain]就会报上面的错。

    在setter方法里面加个判断,如下:

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

    重新执行代码:

    void test4()
    {
        MJDog *dog = [[MJDog alloc] init]; // dog:1
    
        MJPerson *person = [[MJPerson alloc] init]; // person : 1
        [person setDog:dog]; // dog:2
    
        [dog release]; // dog:1
    
        [person setDog:dog];//旧值新值一样,不做任何事
        [person setDog:dog];
        [person setDog:dog];
    
        [person release]; // person : 0  dog : 0
    }
    

    这时候,如果旧值和新值一样,就什么都不做了,根据上面引用计数器的分析,这样写就没问题了。

    4. 完善

    在dealloc里面我们经常见到别人这么写:

    - (void)dealloc
    {
        self.dog = nil;
        [super dealloc];
    }
    

    其实,self.dog = nil就相当于:

    - (void)setDog:(MJDog *)dog
    {
        if (_dog !=nil) { // 如果旧值不为nil
            [_dog release]; //就把旧值release
            //_dog = [nil retain];
            _dog = nil; //并且将指针置为nil
        }
    }
    

    可以看出,和上面我们写的dealloc是一样的,所以推荐使用self.dog = nil这种方式,更简洁。

    5. 总结

    1. 为什么要retain新值?
      只要有一个人使用新值,就要保证新值不被销毁,所以自然要retain一下了。
    2. 为什么要release旧值?
      旧值使用的时候retain过了,现在你不使用它了,肯定要把旧值release一下,否则旧值会一直在内存中,这样就造成了内存泄漏(该释放的对象没有释放)。
    3. 为什么要判断新值旧值是否相等?
      先release旧值后retain新值,旧值和新值是一样的,由于旧值release之后引用计数器就为0,dog被释放了,这时候再[dog retain]就会报如下错:
    -[MJDog retain]: message sent to deallocated instance 0x10058cdf0
    错误意思就是,retain消息发送给一个已经释放掉的对象了(僵尸对象)。
    

    如果是基本数据类型,不用进行内存管理,就很简单了,直接赋值就可以了。
    如果是OC对象,那么它的setter方法要这样写:

    MJPerson.h

    #import <Foundation/Foundation.h>
    #import "MJDog.h"
    
    @interface MJPerson : NSObject
    {
        int _age;
        MJDog *_dog;
    }
    
    - (void)setAge:(int)age;
    - (int)age;
    
    - (void)setDog:(MJDog *)dog;
    - (MJDog *)dog;
    
    @end
    

    MJPerson.m

    #import "MJPerson.h"
    
    @implementation MJPerson
    
    - (void)setAge:(int)age
    {
        _age = age;
    }
    
    - (int)age
    {
        return _age;
    }
    
    - (void)setDog:(MJDog *)dog
    {
        if (_dog != dog) {
            [_dog release];
            _dog = [dog retain];
        }
    }
    
    - (MJDog *)dog
    {
        return _dog;
    }
    
    - (void)dealloc
    {
        self.dog = nil;
        [super dealloc];
    }
    @end
    

    上面是MRC中最原始的也是最麻烦的写法了,随着Xcode编译器的发展,也出现了@synthesize关键字的用法,具体可参考:@synthesize和@dynamic,这里就不详细说了。
    直到现在,ARC下,使用一个简单的@property就可以生成_age成员变量,setter、getter方法声明,setter、getter方法实现。

    三. MRC下的@property

    如果刚才你看了@synthesize和@dynamic,你就会知道,MRC下的@property只会生成setter、getter方法的声明,如果想生成_age成员变量和setter、getter方法的实现还要使用@synthesize关键字。

    下面就看看使用不同的关键字修饰@property并且使用了@synthesize关键字,生成的setter、getter方法实现有什么不同。

    1. 如果使用assign修饰

    @property (nonatomic, assign) int age;
    

    那么生成的就是:

    - (void)setAge:(int)age
    {
        _age = age;
    }
    
    - (int)age
    {
        return _age;
    }
    

    可以发现,使用assign生成的setter方法没有内存管理相关的东西,所以assign一般用来修饰基本数据类型。

    2. 如果使用retain修饰

    @property (nonatomic, retain) MJDog *dog;
    

    那么生成的就是:

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

    可以发现,使用retain修饰,生成的setter方法有内存管理相关的东西,所以retain一般用来修饰对象类型。

    3. 使用@synthesize

    就算在MRC环境下,我们也不会像我上面总结的那样,写那么一大串又原始又复杂的代码,MRC环境下,一般我们这么写:

    #import <Foundation/Foundation.h>
    #import "MJDog.h"
    
    @interface MJPerson : NSObject
    
    @property (nonatomic, assign) int age;
    @property (nonatomic, retain) MJDog *dog;
    
    + (instancetype)person; //工厂方法
    
    @end
    

    上面代码,使用@property生成setter、getter方法的声明

    #import "MJPerson.h"
    
    @implementation MJPerson
    // 自动生成_开头的成员变量和setter、getter方法的实现
    @synthesize age = _age, dog = _dog;
    
    + (instancetype)person
    {
        return [[[self alloc] init] autorelease]; //自动释放
    }
    
    - (void)dealloc
    {
        self.dog = nil;
        [super dealloc];
    }
    @end
    

    上面代码:

    1. 使用@synthesize自动生成_开头的成员变量和setter、getter方法的实现(如果是使用assign修饰的,就直接赋值,如果是使用retain修饰的,就生成相应的带内存管理的setter方法)。

    2. 在MRC里面,就算你使用retain自动生成了内存管理相关的setter方法,在dealloc里面还是要你自己去释放的(因为那两个关键字没帮我们自动生成)。

    3. MRC环境下,我们也会给类添加一个工厂方法,可以自动释放对象,如上代码。使用起来也很简单,这样就不用我们每次手动release了,如下:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            MJPerson *person = [MJPerson person];
        }
        return 0;
    }
    

    四. 体验MRC如何写代码

    #import "ViewController.h"
    
    @interface ViewController ()
    @property (retain, nonatomic) NSMutableArray *data;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //刚开始我们这么写
        NSMutableArray *data = [[NSMutableArray alloc] init];
        self.data = data;
        [data release];
    
        //后来简化
        self.data = [[NSMutableArray alloc] init];
        [self.data release];
    
        //后来又简化
        self.data = [[[NSMutableArray alloc] init] autorelease];
        
        //最后还可以这样写
        //Foundation框架,一般使用类方法创建的对象,内部都已经调用了autorelease
        //也可以这样想:array方法没看到alloc,所以不用release
        self.data = [NSMutableArray array];
    }
    
    //一定要释放
    - (void)dealloc {
        self.data = nil;
        [super dealloc];
    }
    @end
    

    Demo地址:MRC

    相关文章

      网友评论

          本文标题:iOS-内存管理3-MRC

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