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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342