iOS-内存管理3-MRC

一. 初识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 
#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 
#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)