天天看点

内存管理—MRC时代的手动内存管理

内存管理系列文章

内存管理—MRC时代的手动内存管理

内存管理—weak的实现原理

内存管理——autorelease原理分析

内存管理——定时器问题

iOS程序的内存布局

MRC时代的手动内存管理

iOS中是通过【引用计数】来管理OC对象的内存的。

  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,其占用的内存空间会被系统释放。
  • 调用

    retain

    会让OC对象的引用计数+1,调用

    release

    会让OC对象的引用计数-1。

内存管理的原则

  • 当调用

    alloc

    new

    copy

    mutableCopy

    方法返回了一个对象,再不需要这个对象时,要调用

    release

    或者

    autorelease

    来释放它。
  • 想拥有某个对象,就让它的引用计数+1,不想再拥有某个对象,就让他的引用计数-1。

可以通过一下私有函数来查看自动释放池的情况

extern void _objc_autoreleasePoolPrint(void);

下面我们通过案例来分析一波

//*********  main.m   ************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        CLPerson *person = [[CLPerson alloc] init];
        NSLog(@"retainCount ----- %zd",[person retainCount]);
        
    }
    return 0;
}

//*********  CLPerson.h   ************
#import <Foundation/Foundation.h>

@interface CLPerson : NSObject

@end

//*********  CLPerson.m   ************
#import "CLPerson.h"

@implementation CLPerson
-(void)dealloc {
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end
           

我们从在

main.m

里面通过

alloc

创建一个

CLPerson

实例,通过打印可以看到其引用计数为

1

2019-08-27 09:12:45.362995+0800 MRCManager[10928:615055] retainCount ----- 1
           

并且没有看到

person

调用

dealloc

方法,说明在

main

函数结束之后,

person

并没有被释放。那么我们在使用完

person

之后给加上一句

release

,如下

CLPerson *person = [[CLPerson alloc] init];
NSLog(@"retainCount ----- %zd",[person retainCount]);
[person release];
           

这次的打印结果为

2019-08-27 09:12:45.362995+0800 MRCManager[10928:615055] retainCount ----- 1
2019-08-27 09:12:45.363226+0800 MRCManager[10928:615055] -[CLPerson dealloc]
           

可以看到,

person

走了

dealloc

方法,也就是成功被释放了,原因就是通过

release

方法,使得自身的引用计数-1,1 - 1 = 0,然后系统便依据该引用计数的值将

person

释放。OC的内存管理其实原理很简单。

我们知道,Mac命令行项目,

main

函数时从上至下,线性执行,走到

return 0

,整个程序就退出结束了,因此像我们案例中的情景,很容易判断该何时对对象进行

release

操作。

在我们常用的iOS项目里面,由于加入了RunLoop,程序会在

main

函数里面一直循环,直到崩溃,或者手动关闭app。因此当一个对象被创建了之后,它什么时间会被使用,是很难确定的。如果不调用

release

,那么可以保证任何时间使用对象都是安全的,但是带来的问题便是,当对象不再使用之后,它便会一直存留在内存里面,不被释放,这就是我们常说的 【内存泄漏】。

为此,苹果为我们提供了

autorelease

,在每次创建对象的时候调用

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

这样,无需我们手动调用

[person release];

,系统会在某个合适的时间,自动对

person

进行

release

操作,这个“合适的时间”暂且理解成

@autoreleasepool {}

大括号结束的时候。

#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        CLPerson *person = [[[CLPerson alloc] init] autorelease];
        CLPerson *person2 = [[[CLPerson alloc] init] autorelease];
        NSLog(@" @autoreleasepool即将结束");
    }
    NSLog(@" @autoreleasepool已经结束");
    return 0;
}

//********************** 打印信息  *******************
2019-08-27 09:40:29.388495+0800 MRCManager[10970:625654]  @autoreleasepool即将结束
2019-08-27 09:40:29.388727+0800 MRCManager[10970:625654] -[CLPerson dealloc]
2019-08-27 09:40:29.388736+0800 MRCManager[10970:625654] -[CLPerson dealloc]
2019-08-27 09:40:29.388756+0800 MRCManager[10970:625654]  @autoreleasepool已经结束
Program ended with exit code: 0
           

上述案例仅仅针对一个对象这种简单情况来讨论。在iOS实际项目中,往往对象与对象之间是有很多关联的。我们继续给上面的代码添加一个

CLCat

对象

#import <Foundation/Foundation.h>

@interface CLCat : NSObject
-(void)run;
@end

***************************************************

#import "CLCat.h"

@implementation CLCat
-(void)run {
    NSLog(@"%s",__func__);
}

-(void)dealloc {
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end
           

如果

CLPerson

想拥有

CLCat

,则需要对其作如下调整

#import <Foundation/Foundation.h>
#import "CLCat.h"
@interface CLPerson : NSObject
{
    CLCat *_cat;
}
//拥有猫
-(void)setCat:(CLCat *)cat;
//获取猫
-(CLCat *)cat;
@end

***************************************************


#import "CLPerson.h"
@implementation CLPerson

-(void)setCat:(CLCat *)cat {
    _cat = cat;
}

-(CLCat *)cat {
    return _cat;
}

-(void)dealloc {
    [super dealloc];
    NSLog(@"%s",__func__);
}

@end
           

这样就可以通过

setCat

方法,把

CLCat

对象设置到

CLPerson

_cat

成员变量上(拥有);通过

cat

方法拿到成员变量

_cat

(获取)。也就是说

CLPerson

对象可以通过

_cat

指针操纵一个

CLCat

对象了。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLCat *cat = [[CLCat alloc] init];
        CLPerson *person = [[CLPerson alloc] init];
        [person setCat:cat];
        [person.cat run];
        
        [cat release];
        [person release];
        
    }
    return 0;
}

***************** 打印结果  ****************
2019-08-27 10:22:11.086033+0800 MRCManager[11054:643966] -[CLCat run]
2019-08-27 10:22:11.086283+0800 MRCManager[11054:643966] -[CLCat dealloc]
2019-08-27 10:22:11.086294+0800 MRCManager[11054:643966] -[CLPerson dealloc]
Program ended with exit code: 0

           

从打印结果看上去,行得通。但是注意

[person.cat run];

是在

[cat release];

之前。如果是下面这样

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLCat *cat = [[CLCat alloc] init];
        CLPerson *person = [[CLPerson alloc] init];
        [person setCat:cat];

        [cat release];
        [person.cat run];
        [person release];
        
    }
    return 0;
}
           

结果会是这样

内存管理—MRC时代的手动内存管理

报错的原因图中已经表明,所以,只需要确保

[cat release];

[person.cat run];

之后被调用即可。但是实际开发中,

[person.cat run];

何时被调用,是不确定的,并且次数也不确定,也就是说,我们无法确定

[person.cat run];

会于何时在何处被最后一次调用,因此就无法确定

[cat release];

到底该写在哪里。为了保证不出现

EXC_BAD_ACCESS

报错,可以干脆不写

[cat release];

,但这就带来了内存泄漏问题。

问题的本质就是

CLPerson

并没有真正拥有

CLCat

。所谓“真正”拥有,就是指只要

CLPerson

还在,那么

CLCat

就不应该被释放。为此,我们就可以这么做

#import "CLPerson.h"

@implementation CLPerson


-(void)setCat:(CLCat *)cat {
    [_cat retain];//将引用计数+1
    _cat = cat;
}

-(CLCat *)cat {
    
    return _cat;
}

-(void)dealloc {
    //自己即将被释放,不再需要cat了
    [_cat release];
    _cat = nil;
    
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end
           

这样即使有多个CLPerson对象在使用CLCat,也不会出问题了

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //RC+1
        CLCat *cat = [[CLCat alloc] init];
        
        CLPerson *person = [[CLPerson alloc] init];
        CLPerson *person2 = [[CLPerson alloc] init];
        
        //内部 RC+1(setCat) --> RC-1(dealloc)
        [person setCat:cat];
        
        //内部 RC+1(setCat) --> RC-1(dealloc)
        [person2 setCat:cat];
        
        //RC-1,为了对应上面的[CLCat alloc]
        [cat release];
        
        [person.cat run];
        [person2.cat run];
        [person release];
        [person2 release];
        
    }
    return 0;
}
           

CLCat

retainCount

变化过程可判断,最后它一定会变成

,不影响CLCat实例对象的释放,同时,也保证了

CLCat

retainCount

一定是在最后一个

CLPerson

实例对象释放之前(意味着

CLCat

不再被需要了,可以被释放了)被变成

,成功释放。运行结果也可以验证

2019-08-27 10:55:41.799859+0800 MRCManager[11120:657618] -[CLCat run]
2019-08-27 10:55:41.800096+0800 MRCManager[11120:657618] -[CLCat run]
2019-08-27 10:55:41.800111+0800 MRCManager[11120:657618] -[CLPerson dealloc]
2019-08-27 10:55:41.800117+0800 MRCManager[11120:657618] -[CLCat dealloc]
2019-08-27 10:55:41.800123+0800 MRCManager[11120:657618] -[CLPerson dealloc]
Program ended with exit code: 0
           

总的来说,手动管理内存的原则就是:保持实例对象的retainCount平衡,有+1,就有对应的-1,保证最终会变成0,实例对象可以被成功释放。

到目前为止,上面对setCat方法的处理,仍然不够完善。接下来我们继续讨论一下相关细节。 首先看下面的场景

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //person_rc + 1 = 1
        CLPerson *person = [[CLPerson alloc] init];
        
        //cat1_rc + 1 = 1
        CLCat *cat1 = [[CLCat alloc] init];
        
        //cat2_rc + 1 = 1
        CLCat *cat2 = [[CLCat alloc] init];
        
        //cat1_rc + 1 = 2
        [person setCat:cat1];
        
        //cat2_rc + 1 = 2
        [person setCat:cat2];
        
        //cat1_rc - 1 = 1
        [cat1 release];
        
        //cat2_rc - 1 = 1
        [cat2 release];
        
        //cat2_rc - 1 = 0
        //person_rc - 1 = 0
        [person release];
    }
    return 0;
}

**************** 打印结果 ****************
2019-08-27 11:23:20.185060+0800 MRCManager[11164:667802] -[CLCat dealloc]
2019-08-27 11:23:20.185318+0800 MRCManager[11164:667802] -[CLPerson dealloc]
Program ended with exit code: 0
           

打印结果显示,

cat1

产生了内存泄漏。根据代码注释里对各对象

retainCount

的跟踪,可以看出,是因为

person

setCat

方法里设置

cat2

为成员变量的时候,造成了

cat1

最终少进行了一次

release

,从而导致被泄漏。因此需要对

setCat

方法调整如下即可

-(void)setCat:(CLCat *)cat {
    [_cat release];//将之前_cat所指向的对象引用计数-1,不在持有
    [cat retain];//将传进来的对象引用计数+1,保证持有
    _cat = cat;
}
           

上面我们解决了用不同

CLCat

对象进行

setCat

设置所产生的问题。接下来我们还需要看看用同一个

CLCat

对象进行

setCat

设置是否安全,代码如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //person_rc + 1 = 1
        CLPerson *person = [[CLPerson alloc] init];
        
        //cat_rc + 1 = 1
        CLCat *cat = [[CLCat alloc] init];
        
        //cat_rc + 1 = 2
        [person setCat:cat];
        
        //cat_rc - 1 = 1
        [cat release];
        
        
        [person setCat:cat];
        
        
        [person release];
        
    }
    return 0;
}
           

上述代码可以安全走到下图所示的断点处

内存管理—MRC时代的手动内存管理

我们继续执行,就会在

setCat

方法里看到如下报错

内存管理—MRC时代的手动内存管理

可以看到

[cat retain]

报了

EXC_BAD_ACCESS

错误,说明

cat

此时已经被释放了。我们来分析一下,进入此方法是,

cat

对象的

retainCount

1

,当再次把cat对象传

setCat

方法是,由于

person

_cat

指向的也是

cat

,因此

[_cat release]

实际上就会导致

cat

retainCount-1

,也就是

1-1=0

,所以

cat

被系统释放。因此后面的代码再次使用

[cat retain]

便造成了野指针错误。因此解决办法是需要对传如的

cat

对象判断一下,如果等于当前

_cat

,就不需要执行引用计数的操作了,修改代码如下

-(void)setCat:(CLCat *)cat {
    if (cat != _cat) {//只有当传进来的对象跟当前对象不同,才需要进行后面的操作
        [_cat release];//将之前_cat所指向的对象引用计数-1,不在持有
        [cat retain];//将传进来的对象引用计数+1,保证持有
        _cat = cat;
    }
    
}
           

这样,对于

setCat

方法的处理放啊就完善了。下面我贴一份完整的代码供参考

******************* CLPerson.h ******************
#import <Foundation/Foundation.h>
#import "CLCat.h"

@interface CLPerson : NSObject
{
    CLCat *_cat;
    int _age;
}
//拥有猫
-(void)setCat:(CLCat *)cat;
//获取猫
-(CLCat *)cat;
@end

******************* CLPerson.m ******************


#import "CLPerson.h"

@implementation CLPerson
-(void)setCat:(CLCat *)cat {
    if (cat != _cat) {//只有当传进来的对象跟当前对象不同,才需要进行后面的操作
        [_cat release];//将之前_cat所指向的对象引用计数-1,不在持有
        [cat retain];//将传进来的对象引用计数+1,保证持有
        _cat = cat;
    }
    
}

-(CLCat *)cat {
    
    return _cat;
}


-(void)dealloc {
    //自己即将被释放,不再需要cat了
//    [_cat release];
//    _cat = nil;
    self.cat = nil;//相当于上面两句的效果
    
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end


*****************CLCat.h ***************
#import <Foundation/Foundation.h>
@interface CLCat : NSObject
-(void)run;
@end

*****************CLCat.m ***************
#import "CLCat.h"
@implementation CLCat
-(void)run {
    NSLog(@"%s",__func__);
}

-(void)dealloc {
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end
           

对于非OC对象类型的成员变量,就不需要考虑内存管理的问题了,例如

@interface CLPerson : NSObject
{
    int _age;
}
-(void)setAge:(int)age ;
-(int)age;
@end

@implementation CLPerson
-(void)setAge:(int)age {
    _age = age;
}

-(int)age {
    return _age;
}
@end
           

上面过程中,包含了非ARC时代进行手动内存管理的全部核心点。后来,随着Xcode的逐步发展,编译器自动帮我们生成了很多代码,让我的代码书写更加简洁。

编译器的进化

(1)

@property

+

@synthesize

@property (nonatomic, retain) cat;

的作用:自动声明

getter

setter

方法
-(void)setCat:(CLCat *)age ;
-(CLCat *)cat;
           

@synthesize cat = _cat;

的作用:
  • 为属性

    cat

    生成成员变量

    _cat

{
   CLCat *_cat;
}
           
  • 自动生成

    getter

    setter

    方法的实现
-(void)setCat:(CLCat *)cat {
   if (cat != _cat) {
       [_cat release];
       [cat retain];
       _cat = cat;
   }
}

-(CLCat *)cat {
   return _cat;
}
           

(2)

@property

+

@synthesize

后来苹果更进一步,连

@synthesize

都不需要我们写了,一个

@property

搞定
  • 成员变量的创建
  • getter

    setter

    方法声明
  • getter

    setter

    方法实现

但是需要注意的是,

@property

并不帮我做

dealloc

里面的处理(对不需要使用的成员变量进行释放),因此

dealloc

方法还是需要我们手动去写的。

其实进入了ARC时代,iOS的内存管理底层机制并没有变化,只不过很多内存管理的代码ARC(编译器)自动替我们写好而已。好了,上古时期的MRC手动内存管理就介绍到这里。