Objective-C中的内存管理及MRC

Posted __Sunshine_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Objective-C中的内存管理及MRC相关的知识,希望对你有一定的参考价值。

本文主要介绍以下几部分:

一、内存管理的概念

1、为什么要管理内存?
因为移动设备的内存极其有限,不回收不再使用的对象和变量会耗费内存使系统崩溃。

2、Objective-C中内存管理的范围:管理任何继承自NSObject的对象。不管理基本数据类型。
因为基本数据类型储存在栈区,由系统管理;而OC对象存储在堆区,其内存不连续,不能自动释放,所以系统不负责管理。所以也可以理解为OC的内存管理就是对 堆区中对象的内存管理。

3、相关术语:
对象所有权:任何可以继续存在的对象都有一个或者多个所有者,任何自己创建(alloc, new或 copy的方式)的对象都归自己所有,也可用retain来获得一个对象的所有权。
对象的引用计数器(reference count):每个对象都有自己的引用计数器,用来记录对象被引用(不等于被指针指向)的次数,并被系统用来判断对象是否要被回收(唯一的依据)。刚创建时,默认 引用计数器 = 1,当 引用计数器 = 0时,对象被销毁、回收空间(例外:对象值为nil时, 引用计数器 = 0也不回收)。

每个OC对象内部,都有4个字节的存储空间来存储引用计数器。
如何操作引用计数器:

对象的销毁:当某个对象的引用计数器 = 0时,它要被系统销毁、回收所占的内存。
当对象要被销毁时,系统会自动发送一条dealloc消息给对象(等于告诉对象:你要挂了,你还有什么遗言),对象可以直接调用[super dealloc]销毁掉(没遗言了,直接走),但一般会重写dealloc方法(写点遗嘱),来释放相关的资源(一无所有地来,也一无所有地走)。
一旦对象被回收了,就不能再引用它所占据的存储空间了(否则程序崩溃,也称野指针错误)。

内存管理分类:
包括有MRC(Manual Reference Counting,手动管理,ios4.1之前)、ARC(automatic Reference Counting,自动引用计数,iOS4.1之后的新计数,推荐使用)、垃圾回收(garbage collection,iOS不支持)。

实际使用ARC(系统默认),但要理解MRC。

二、 MRC下的内存管理

手动改为MRC模式:

内存管理的原则:
只要还有人引用这个对象(引用计数器 >= 1),对象就不会被回收;
要想引用这个对象,应让引用计数器+1;
不想引用这个对象,应让引用计数器-1;
谁创建(alloc, new, copy),谁release;
谁retain,谁release;
即:有始有终,有增必有减。

内存管理内容:
野指针(僵尸对象),内存泄露

1、单个对象的内存管理

1)防止野指针错误
野指针错误是:访问了一块坏的内存(已经被回收,不可用)。
僵尸对象:所占内存已经被回收的对象,僵尸对象不能再被使用。(为了效率,Xcode默认是不检测僵尸对象)

//开启僵尸对象检测前
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) 

    void *p = 0;
    @autoreleasepool 
        Person *per = [Person new];
        NSLog(@"before release, per retainCount:%ld", [per retainCount]);
        [per release];
        NSLog(@"after release, per retainCount:%ld", [per retainCount]);

    
    return 0;

//输出结果:
before release, per retainCount:1
after release, per retainCount:1

开启僵尸检测:

同样是以上代码,开启僵尸对象检测后运行程序则报错:

注意
i:对比:

ii:不能使用[p retain]使僵尸对象起死回生。
Iii:为避免使用僵尸对象,在对象被dealloc销毁之后,原指针赋值为nil:

Person *per = [Person new];
[per release];
per = nil;
NSLog(@"per retainCount:%ld", [per retainCount]);
//输出结果:per retainCount:0

2)防止内存泄露
内存泄露的本质:对象的retainCount不为0,但已经没有指针指向对象,使得对象的retainCount在程序结束后都不能等于0,不能被系统销毁、回收。
引起内存泄露的情况有:
i:retain和release个数不匹配

//  main.m
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) 
    void *p = 0;
    @autoreleasepool 
        
        Person *per = [Person new]; //创建对象,retainCount = 1
        [per retain];               //retain 一次,retainCount = 2
        [per retain];               //retain 两次,retainCount = 3
        [per release];              //release 一次,retainCount = 2
        NSLog(@"per retainCount:%ld", per.retainCount);
    
    return 0;

//输出结果:per retainCount:2

程序结束后对象得不到释放,造成内存泄露。

Ii:对象使用的过程中被赋值了nil或者指向其他对象

//  main.m
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) 

    void *p = 0;
    @autoreleasepool 
        Person *per = [Person new]; //创建对象,retainCount = 1
        NSLog(@"per retainCount:%ld", per.retainCount);
        //设置对象为nil,实际是让per指向了一个特殊的地址(无效地址)
        per = nil;

        //对nil的release无效,也无法使原对象的retainCount减1
        [per release];
        NSLog(@"per retainCount:%ld", per.retainCount);  
    
    return 0;

//运行结果:
per retainCount:1
per retainCount:0

Iii:在函数或者方法中不当的使用retain或者release造成的内存泄露。
和情况 i 类似。

2、多个对象的内存管理

1)
如例子,有两个类Person类和Car类:

//  Person.h
#import <Foundation/Foundation.h>
#import "Car.h"
@interface Person : NSObject

    Car *_car;              //人拥有一辆车,Car类的实例作为Person的成员变量

-(void)setCar:(Car *)car;   //设置车
-(void)drive;               //开车的行为
@end
//  Person.m
#import "Person.h"
@implementation Person
-(void)setCar:(Car *)car
    _car = car;

-(void)drive
    [_car run];

-(void)dealloc            //重写dealloc方法
    NSLog(@"person dealloc");
    [super dealloc];

@end
//  Car.h
#import <Foundation/Foundation.h>
@interface Car : NSObject
-(void)run;     
@end
//  Car.m
#import "Car.h"
@implementation Car
-(void)run
    NSLog(@"car is running");

-(void)dealloc            //重写dealloc方法
    NSLog(@"car dealloc");
    [super dealloc];

@end
//  main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Car.h"
int main(int argc, const char * argv[]) 

    void *p = 0;
    @autoreleasepool 
        Person *per = [Person new];
        Car *car = [Car new];

        [per setCar:car];   //设置人拥有的车
        [per drive];        //人开车

        [car release];      //车先销毁
        [per release];      //人再销毁
    
    return 0;

//运行结果:
car is running
car dealloc
person dealloc

看似合理,却隐含问题。问题在于在[car release]之后,车已经被销毁,但此时per的成员变量_car仍指向一块被回收的内存(僵尸对象),就成了野指针,若不慎操作,如在[car release]之后又调用了[per drive],又访问了那块被回收的内存,则引起野指针错误:

[car release];      //车先销毁
[per drive];        //人开车
[per release];      //人再销毁

//运行结果:

所以解决办法是,要让[car release]之后,车的retainCount不为0,这样车就不会被销毁,人还可以开车,做法是在Person类的setCar方法中使得car的retainCount加1,但要注意,既然是Person类给car的retainCount加1,也应由Person类来负责给它减1,而减1的行为可以在Person类的实例被销毁前再完成,即在dealloc中完成。所以Person类的setCar和dealloc方法改写如下:

//  Person.m
-(void)setCar:(Car *)car
    _car = [car retain];    //使car的retainCount加1

-(void)dealloc        
    NSLog(@"person dealloc");
    [_car release];         //使car的retainCount减1
    [super dealloc];

这样就没问题了么?
结合上面单个对象内存泄露的情况,除了进行retain和release操作的个数不相等(在main函数或者其他的方法或函数)外,还可能造成内存泄露的情况是:指针在对象被销毁前指向nil或者其他对象。如:上例中,在car对象被销毁前,又设置了car2对象被person对象作成员,则原car对象得不到释放,造成内存泄露:

        Person *per = [Person new];
        Car *car = [Car new];

        [per setCar:car];   //设置人拥有的车

        [car release];      //车先销毁

        Car *car2 = [Car new];
        [per setCar:car2];

        [car2 release];     //car2被销毁
        [per release];      //人再销毁
//输出结果:
person dealloc
car dealloc   //只有car2被释放,car未被释放

解决方法是:要让Person类的实例的_car指针指向其他Car对象前,先把原指向的对象release,即使刚开始的第一次release消息发给了nil也是合法的。即把setCar函数改写为:

-(void)setCar:(Car *)car
    [_car release];
    _car = [car retain];    //使car的retainCount加1

但此时要注意的问题是:如果已经指向了car对象,之后又再次将car对象作为Person类的setCar的参数,此时要是对car发送了release消息,则car对象已经被销毁,而接着发送retain消息给car对象(也就是刚刚销毁掉的对象,也叫僵尸对象),则又是野指针错误。所以要在发送release给原_car指向的对象前,先判断传进函数的参数car对象与原_car对象是否是同一个对象:

-(void)setCar:(Car *)car
    if (_car != car)           //1、先判断两者是否为同一对象
        [_car release];         //2、如果两者不同,先释放上一个对象
        _car = [car retain];    //3、使形参的retainCount加1
    
    // 如果两者为同一对象,则什么都不做

对其他OC对象,在setter方法中也是要执行以上三步,而对于基本数据类型(系统自动管理其内存),则可以直接将形参赋值给成员变量。

2)@property 参数

Xcode4.4之后,@property 的使用可以:生成一个以“_”开头的属性、生成set和get方法的声明和实现。
在@property中可以添加参数:
格式: @property (参数1,参数2) 数据类型 方法名

可以修改setter、getter的方法名,但一般只用于 BOOL 类型,把getter方法名以“is-”开头。如

@interface Person : NSObject
//修改setter、getter的方法名
@property (nonatomic, assign, setter=setVip:, getter=isVip) BOOL vip;
@end
//  main.m
#import <Foundation/Foundation.h>
#import "Person.h"
    @autoreleasepool 
        Person *per = [Person new];
        [per setVip:YES];           //OK
        per.setVip = YES;           //error!
        NSLog(@"%d", [per isVip]);  //OK
        NSLog(@"%d", per.isVip);    //OK
    
    return 0;

3)@class

i)@class的作用和使用
它可以简单地引用一个类,如在 Person.h 中将 #import “Car.h” 改为

@class Car;
//  Person.h
#import <Foundation/Foundation.h>
@class Car;
@interface Person : NSObject

    Car *_car;              //人拥有一辆车

@end

它的作用是告诉编译器:Car是一个类,没了,不要期望我告诉你关于它的更多信息(比如实例变量和方法)。推荐在类的 .h文件上使用@class。
但要记得在 Person.m 文件中使用 #import “Car.h” ,否则不能使用 Car类的实例变量。
经在Xcode6.1上测试,如果没#import “Car.h”也可以使用Car类的方法:

ii)为什么要用@class
可以通过对比这两种引用一个类的方法来说明@class的优势:

特别是对于循环依赖关系来说,@class 有着 #import 所没有的优势:
如当Person类的对象per有一辆car,所以需要引用Car类;Car类的对象car需要有一位person作为主人owner,所以需要引用Person类。所以这两个对象的关系如图:

此时只能用 @class 引用类,否则(用 #import)则编译报错。

4)循环retain问题
在上面循环依赖这类情况,

//  Person.h
#import <Foundation/Foundation.h>
@class Car;
@interface Person : NSObject
@property (nonatomic, retain) Car *car;     //使用了retain
-(void)setCar:(Car *)car;   //设置车
-(void)drive;               //开车的行为
@end
//  Car.h
#import <Foundation/Foundation.h>
@class Person;
@interface Car : NSObject
@property (nonatomic, retain) Person *owner;    //使用了retain
-(void)run;
@end
//  main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Car.h"
int main(int argc, const char * argv[]) 
    @autoreleasepool 
        Person *per = [Person new];
        Car *car = [Car new];

        per.car = car;      //互相设置成员变量
        car.owner = per;

        [per drive];
        [car release];      //各自release
        [per release];
        NSLog(@"per.retainCount = %ld, car.retainCount = %ld", per.retainCount, car.retainCount);
    
    return 0;

//运行结果:
car is running
per.retainCount = 1, car.retainCount = 1

所以知道程序结束,两个对象的 retainCount 仍不为0,都得不到释放,造成内存泄露。示意图如下:

解决办法:两端互相引用时,一端用 retain ,一端用 assign (注意:在setter中使用assign,就不用在dealloc中release了):

3、NSString类的内存管理

看一下代码及输出:

NSString *str_1 = @"a";
NSString *str_2 = [NSString stringWithString:str_1];
NSString *str_3 = [NSString stringWithFormat:@"c"];
NSString *str_4 = [[NSString alloc] initWithString:@"d"];
NSString *str_5 = [[NSString alloc] initWithFormat:@"e"];
NSString *str_6 = [[NSString alloc] init];

NSLog(@"str_1 : %p ,      retainCount : %ld", str_1, [str_1 retainCount]);
NSLog(@"str_2 : %p ,      retainCount : %ld", str_2, [str_2 retainCount]);
NSLog(@"str_3 : %p ,      retainCount : %ld", str_3, [str_3 retainCount]);
NSLog(@"str_4 : %p ,      retainCount : %ld", str_4, [str_4 retainCount]);
NSLog(@"str_5 : %p ,      retainCount : %ld", str_5, [str_5 retainCount]);
NSLog(@"str_6 : %p ,      retainCount : %ld", str_6, [str_6 retainCount]);
//输出结果:
str_1 : 0x1000020a0 ,      retainCount : -1
str_2 : 0x1000020a0 ,      retainCount : -1
str_3 : 0x6315 ,      retainCount : -1
str_4 : 0x1000020e0 ,      retainCount : -1
str_5 : 0x6515 ,      retainCount : -1
str_6 : 0x7fff7651bd00 ,      retainCount : -1

由输出的地址可以看出,str_1、str_2、str_4都是存储在常量内存区,而str_6存储在栈区,而对所有的NSString对象发送retainCount信息都是无意义的,可能因为系统对NSString类的对象有特殊的处理,不需要我们对它们进行内存管理。

4、自动释放池及autorelease

自动释放池是在程序运行过程中创建的、以栈结构(先进后出)存在的
与Java的全自动化GC不同,自动释放池是半自动化的管理机制。

在iOS5.0之前是创建和释放的:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool release];     //[pool drain]; mac上

而iOS5.0之后是:

@autoreleasepool //创建//销毁

Autorelease

可以在自动释放池内使用autorelease,作用是暂时保存某个对象,然后在自动释放池销毁的时候对每个对象发送release消息(注意只是发送release消息,能不能销毁对象得看对象的retainCount是否为0)。使用的好处:确保对象能正确释放、又能返回有效的对象,而且使用者不需要再关心调用release的时间、对象被释放的时间

1)autorelease的基本用法
如:

int main(int argc, const char * argv[]) 
    @autoreleasepool                                     //自动释放池开始创建
        Person *per = [[[Person alloc] init] autorelease];//1、把新创建的对象放到自动释放池,并返回对象本身给per
                                                         //2、对象的计数器不受影响,per.retainCount = 1
     // 3、自动释放池被销毁时,发送release给per对象,相当调用[per release],此时per.retainCount = 0,per对象被释放
    return 0;

2)autorelease的原理
autorelease只是延迟了对 release 的调用。每个对象调用 autorelease 方法的时候,系统只是把该对象放入当前的自动释放池,当释放池要销毁的时候,池中所有的对象都被调用了 release 方法。

3)autorelease使用注意:
i: 要把对象放进自动释放池,要在autoreleasepool中、显式地调用 autorelease ,两者缺一不可。至于对象的创建,无论是在释放池内还是外创建的都没关系。

ii: 自动释放池可以嵌套使用。但对于一个对象,在每个释放池只能放进一次(即只调用一次 autorelease 方法,若不慎调用多次,不报错,也只放进一次),若要在不同的释放池,则在不同的释放池各调用一次 autorelease,而且放进的是当前的自动释放池栈顶的释放池:

int main(int argc, const char * argv[]) 
    @autoreleasepool  //pool_1
        Person *per = [[Person alloc] init];//创建对象,per.retainCount = 1
        @autoreleasepool  //pool_2
            @autoreleasepool  //pool_3
                [per retain];           //per.retainCount = 2
                [per autorelease];      //放进第二个释放池,per.retainCount = 2
                NSLog(@"per.retainCount = %ld", per.retainCount);
             //pool_1销毁,调用[per release]一次,per.retainCount = 1
         //pool_2销毁,但因对象per没放进pool_2,不发送release消息给对象per
    //pool_3销毁,调用[per release]一次,per.retainCount = 0,调用[per dealloc]把对象销毁
    return 0;


iii: 自动释放池中不适合放进占用内存比较大的对象或者大量循环操作。
因为延迟释放机制,要是放进大内存的对象或大量循环,则会造成内存峰值的上升。

4)autorelease 的应用:
经常用来在类方法中快速创建一个对象:

//  Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
+(Person *)person;                              //声明类方法person
@end

//  Person.m
#import "Person.h"
@implementation Person
+(Person *)person                              //实现类方法person
    return [[[Person alloc] init] autorelease]; //创建新对象、初始化、放进释放池,返回对象本身

@end

//  main.m
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) 
    @autoreleasepool 
        Person *per2 = [Person person];         //调用类方法person,得到一个已经放进释放池的对象
        return 0;

注意这种情况要在释放池内部调用类方法person,否则不能放进释放池。

但上面的快速创建对象是有缺陷的。我们先看下类方法new、alloc的返回值

//Allocates a new instance of the receiving class, sends it an init message, and returns the initialized object.
+ (instancetype)new;    
//Returns a new instance of the receiving class.
+ (instancetype)alloc;  

它们都是NSObject的类方法,返回值类型都是instancetype,在方法中返回的是与接收信息的类相同类型的新对象,使得继承自NSObject类的类在调用这两个类方法后都能得到与自己类型相同的对象。而上面Person类创建了快速创建对象的类方法person,若它的子类(如Student类)也要快速创建对象,只能再次创建个类方法student,否则调用[Student person]只能返回一个Person类型的对象,无法使用Student类特有的属性和方法。所以要将上面的类方法修改如下:

+(instancetype)person                              //实现类方法person,返回值类型为instancetype
    return [[[self alloc] init] autorelease];       //创建新对象、初始化、放进释放池,返回对象本身

注意返回值类型instancetype不能改为id,因为id不进行类型检测,instancetype要更安全。

小结:

以上是关于Objective-C中的内存管理及MRC的主要内容,如果未能解决你的问题,请参考以下文章

Objective-c的内存管理MRC与ARC

Objective-C内存管理之MRC

Objective-C内存管理之MRC

内存管理-MRC与ARC详解

Objective-C内存管理机制

ARC内存管理机制详解