OC-内存管理

时间:2022-04-27 14:24:45

解铃还须系铃人

--1--内存管理的原理及分类
1.1 内存管理的原理
1.2 内存管理的分类
--2--手动内存管理
2.1 关闭ARC的方法
2.2 手动管理(MRC)快速入门
--3-- 内存管理的原则
3.1 内存管理的原则
3.2 内存管理研究的内容
--4-- 单对象内存管理
4.1 单个对象的野指针问题
4.2 避免使用僵尸对象的方法
4.3 对象的内存泄漏
--5-- 多个对象内存管理
5.1 多个对象的野指针问题
5.2 多个对象内存泄漏问题
--6-- set方法内存管理
6.1 set方法存在的问题
6.2 不同类型的setter写法
--7-- autorelease
7.1 autorelease是什么
7.2 为什么会有autorelease
7.3 autorelease基本用法
7.4 autorelease的原理
7.5 autorelease什么时候被释放
7.6 autorelease使用注意

--------------------------------------

【写在开头】

『使用这个标题,“解铃还须系铃人”好像有点不正式。但这里,只是想突出一个内存管理的原则:“谁创建,谁释放”。iOS的内存管理和Java等语言的垃圾回收机制不同,Java的垃圾回收机制是运行时的特性,由jvm去回收释放内存。这里不谈Java,回到iOS的内存管理,目前创建项目默认就是ARC机制,而以前是MRC机制,需要程序员自己手动去回收释放内存,注意ARC和MRC都是编译器的特性,一个移动设备的内存是有限的,所以学习iOS的内存管理非常必要。

OC内存管理的范围:

管理任何继承自NSObject的对象,对其他的基本数据类型无效。

本质原因是因为对象和其他数据类型在系统中的存储空间不一样,如局部变量最主要存放在栈中。而对象存储在堆中,当代码块结束时,这个代码块中涉及的所有的局部变量会被回收,指向对象的指针也被回收,此时对象已经没有指针指向,但依然存在于内存中,就会造成内存泄漏。

下面的内存管理内容主要是MRC机制』

--1--内存管理的原理及分类

1.1 内存管理的原理

1)对象的所有权及引用计数

  对象所有权概念:

  任何对象都可能拥有一个或多个所有者。只要一个对象至少还拥有一个所有者,它就会继续存在。

OC-内存管理

  Cocoa所有权策略:

  任何自己创建的对象都归自己所有,可以使用名字以“alloc"或”new“开头或名字中包含”copy“的方法创建对象,可以使用retain来获得一个对象的所有权。

  对象的引用计数器:

  每个OC对象都有自己的引用计数器,是一个整数表示对象被引用的次数,即现在有多少东西在使用这个对象。对象刚被创建时,默认计数器值为1,当计数器的值变为0时,则对象销毁。

2)引用计数器的作用

  引用计数器(retainCount):标志被引用的次数,存储当前对象有几个使用者,是判断对象是否回收的依据。

  (例外:对象值为nil时,引用计数器为0,但不回收空间,因为没有分配空间)

3)对引用计数器的操作:

  retain消息:使计数器+1,方法返回对象本身

  release消息:使计数器-1

  retainCount消息:获得当前对象的retainCount的值

4)对象的销毁:

  当retainCount为0时,对象被销毁,内存被回收。对象被销毁时,系统自动向对象发送一条dealloc消息,一般会重写dealloc消息,在这里释放相关的资源,dealloc就像是对象的“临终遗言”。

  一旦重写了dealloc方法就必须调用[super dealloc](注意不能直接调用dealloc方法)。

  一旦对象被回收了,那么他所占据的存储空间就不再可用,坚持使用会导致程序崩溃(野指针错误)。

注意:

1)如果对象的计数器不为0,那么在整个程序运行过程中,它占用的内存就不可能被回收(除非整个程序已经退出)

2)任何一个对象,刚创建的时候,引用计数器都为1,当使用alloc、new或者coppy创建一个对象时,对象的引用计数器默认是1

从此可以看出:

回收一个对象与否的标记就是引用计数器是否为0.

1.2 内存管理的分类

OC内存管理分为3类:

  1)Manual Reference Counting (MRC,手动管理,在开发iOS4.1之前的版本的项目时,需要自己负责使用引用计数来管理内存,比如要手动retain、release、autorelease等,而在其后的版本可以使用ARC,让系统自己管理内存)

  2)Automatic Reference Counting(ARC,自动引用计数,iOS4.1之后推出)

  3)garbage collection(垃圾回收)IOS不支持垃圾回收

ARC作为苹果新提供的技术,苹果推荐开发者使用ARC技术来管理内存。

--2--手动内存管理

2.1 关闭ARC

创建一个项目时,默认是ARC的(自动内存管理),手动把ARC项目改成MRC项目的方法之一如下:

a.选中项目,此时Xcode右侧会出现如图设置信息

OC-内存管理

b. 在同时选中Build Settings和Levels时在右侧搜索框搜索auto,此时会出现ARC设置

OC-内存管理

c.把ARC设置中目标target下的Yes设置为No,其他相应的Yes也变为No,此时就是MRC管理

OC-内存管理

2.2 手动管理(MRC)快速入门

内存管理的关键是判断对象是否被回收?

重写dealloc方法,

代码规范

1)要调用父类的dealloc方法[supper dealloc],而且要放到最后,意义是:先释放子类占用的空间再释放父类占用的空间

2)对self(当前)所拥有的其他对象做一次release操作

 -  (void) dealloc

{

[_car release]; //对象关联的对象

[super dealloc];

}

注意:

一旦对象被回收了,它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针)为了防止调用出错,也可以将“野指针”指向nil(0);

--3-- 原则

3.1 内存管理的原则

1)原则:

  如果对象还有使用者,就不应该回收;

  如果你想使用这个对象,那么就应该让这个对象的引用计数器+1;

  当你不想使用这个对象时,应该让对象的引用计数器-1;

2)谁创建,谁release

  1.如果通过alloc, new, coppy来创建一个对象,那么久必须调用对应的release或者autorelease方法

  2.谁创建谁负责

3)谁retain,谁release

  只要你调用了retain,无论这个对象是如何生成的,你都需调用release

总结:

有始有终,有加就应该有减,让某个对象计数器加1,就应该让其在最后-1

对象如果不再使用了,就应该回收它的空间,防止造成内存泄漏

3.2 内存管理研究的内容

1)野指针(僵尸对象)

野指针:

  1)定义的指针变量没有初始化

  2)指向的空间已经被释放了

2)内存泄漏

内存泄漏:

Person *p = [Person new];

//变量p存储在栈区

//[Person new]; 对象存储在堆区

如果栈区的p已经被回收,而堆区的空间还没有释放,堆区的空间就造成了泄漏

--4-- 单对象内存管理

4.1 单个对象的野指针问题

野指针错误:访问了一块不可再用的内存

僵尸对象:所占的内存已经被回收的对象,僵尸对象不能再被使用。(默认情况下Xcode为了提高编码效率,没有开启僵尸对象检测。若要检测,需先打开僵尸对象检测)

空指针:没有指向任何对象的指针: Person *p = nil;

   给空指针发送消息不会有反应

OC-内存管理

4.2 避免使用僵尸对象的方法

为了防止不小心调用了僵尸对象,可以将对象赋值nil(对象的空值)

Dog *dog = [[Dog alloc] init];

[dog release]; //引用计数-1

dog = nil; //空指针

[dog retain]; //给nil发送任何消息,都不会有效果。所以僵尸对象检测不会报错

4.3 对象的内存泄漏

情景1:

//创建完成使用后,没有release

Dog *d = [Dog new]; //1
//如果对象没有被回收,就造成了内存泄漏

情景2:

没有遵守内存管理的原则

Dog *d = [Dog new]; //引用计数为1

[d retain]; // 计数+1 = 2

[d release]; //计数 - 1 = 1
//计数不为0,不能被回收

情景3:

不当的使用了nil

Dog *d = [Dog new];

d = nil; //指针指向nil

[d run]; //nil run不会有效果

[d relese]; //nil release

//而此处不能回收,因为Dog对象的引用计数还是1

情景4:

在方法中对传入的对象进行了retain,而调用时没有release

- (BOOL)compareColorWithOther:(Dog *)dog{

    [dog retain]; //此处对象引用计数 + 1

    return YES;

}

//而如果在后面没有相应的release,则同样会造成内存泄漏

--5-- 多个对象内存管理

5.1 多个对象的野指针问题

/**
情景:
Person对象拥有一个Car的对象属性
*/
Person *person = [[Person alloc] init]; //引用计数为1
Car *car = [[Car alloc] init]; //计数为1
        
car.speed = ; //car对象拥有速度属性
        
person.car = car; //将Person对象属性赋值为car
        
[car release]; //car对象计数释放一次 - 1 -->变成了0 [person goByCar]; //此时car变成了一个僵尸对象-->car已经是野指针。但是goByCar中还使用着car对象,这样开启检测后就会抛出运行时错误

5.2 多个对象内存泄漏问题

针对 5.1 中存在的问题。

如果在[car  release]之后,不小心再使用了car对象,就会造成野指针访问错误。所以此处解决这个问题的方法是,在Person类中重写setter方法,在setter方法中将传过来的car对象retain一次。

这样,调用时,[car release]后还可以使用car对象,并在Person的dealloc中将_car对象release一次。

只是这样的解决方法还是不严谨的,正确的写法应该是下面 6.2 中的写法。

--6-- set方法内存管理

6.1 set方法存在的问题

原对象无法释放造成了内存泄漏

根据内存管理的原则:

谁创建,谁release

Car *car = [[Car alloc] init];

Car *bmw = [[Car alloc] init]; //新对象

//谁创建,谁release
[bmw release];
[car release]; 

按照 5.2 中的写法,直接在setter方法中将对象的引用计数+1。但像这样有两个对象时,那么旧对象就不能释放了,因为旧对象在setter中之前就retain了一次,变成了2,而在Person中只是将包含的_car对象release一次,这样旧对象就又造成了内存泄漏。

所以setter中正确的写法应该是这样:

- (void)setCar:(Car *)car{

    if (_car != car){ //如果是新的对象,则先把旧的对象release一次,再将新的对象赋值_car

    [_car release]; //初次是[nil release];

    _car = [car retain]; //再让新的对象引用计数+1

    }

}

Person的dealloc中,只要让成员对象release一次就行了

- (void)dealloc{

    [_car release]; //relase成员属性

    NSLog(@"Person被回收");

    [super dealloc];

}

6.2 不同类型的setter写法

1)基本数据类型:直接赋值(因为基本数据类型由系统回收)

//基本数据类型直接赋值即可
//int float double long struct enum -(void)setAge:(int)age { _age = age; }

2)OC对象类型

//对象类型需手动释放其内存
-(void)setCar:(Car *)car { //1.先判断是不是新传进来的对象 if (car != _car){ //2.对旧对象做一次release [_car release]; //若没有旧对象,则没有影响 //3.对新对象做一次retain,并且赋值给实例变量 _car = [car retain]; } }

总结:

setter的内存管理:

原则:如果在一个类中有其他类的对象(关联关系),在setter中,要判断是否是同一个对象。如果是不同对象则先release旧值,再retain新值。

--7-- @autoreleasepool

7.1 @autoreleasepool是什么

autoreleasepool自动释放池

(1)在iOS程序运行过程中,会创建无数个“池子”,这些“池子”以栈结构的形式存在。

(2)当一个对象调用autorelease时,会将这个对象放到位于栈顶的释放池中 。

自动释放池的创建方式

1)iOS5.0以前的创建方式

NSAutoreleasePool *pool=[[NSAutoreleasePool alloc]init];

……………….

[pool release];//[pool drain];用于mac

2)iOS5.0以后

@autoreleasepool

{ //开始代表创建自动释放池

  //

} //结束代表销毁自动释放池

autorelease

是一种支持引用计数的内存管理方式

它可以暂时的保存某个对象(object),然后在内存池自己的排干(drain)的时候对其中的每个对象发送release消息。

注意,这里只是发送release消息,如果当时的引用计数(reference-counted)依然不为0,则该对象依然不会被释放。可以用该方法来保存某个对象,但也要注意保存之后要释放该对象。

7.2 为什么会有autorelease

OC的内存管理机制中比较重要的一条规律是:谁申请,谁释放

考虑这种情况,如果一个方法需要返回一个新建的对象,该对象何时释放?

方法内部是不会写release来释放对象的,因为这样做会将对象立即释放而返回一个空对象:调用也不会主动释放该对象的,因为调用者遵循“谁申请,谁释放”的原则,那么这个时候,就发生了内存泄露。

不使用autorelease存在的问题

针对这种情况,Objective-C的设计了autorelease,既能确保对象能正确释放,又能返回有效的对象。

使用autorelease的好处

(1)不需要再关心对象释放的时间

(2)不需要再关心什么时候调用release

7.3 autorelease基本用法

基本用法

(1)会将对象放到一个自动释放池中

(2)当自动释放池被销毁时,会对池中的所有对象发送一次release

(3)返回对象本身

(4)调用完autorelease方法后,对象的计数器不受影响(销毁时影响)

在autorelease的模式下,下述写法是合理的,即可以正确返回结果,也不会造成内存泄露

ClassA *Func1(){

Class *obj=[[ClassA alloc]init autorelease];

return obj;

}
int main(int argc, const char * argv[]) {
Person *p = [[Person alloc] init];
@autoreleasepool { //注意:加入到自动释放池中以后,引用计数不会变化
[p autorelease]; //把对象p加入到自动释放池中
NSLog(@"p.retainCount = %lu", p.retainCount); //1
//[p release]; //无需手动release
}
return ;
}

重写delloc

@implementation Person

//重写dealloc
- (void)dealloc{
NSLog(@"%@对象释放", self);
[super dealloc];
}
@end

输出-->

OC-内存管理

7.4 autorelease的原理

autorelease实际上只是把对release的调用延迟了,对于每一个Autorelease,系统只是把该Object放入了当前的Autorelease pool中,当该pool被释放时,该pool中的所有Object会被调用Release。

7.5 autorelease什么时候被释放

对于autorelease pool本身,会在如下两个条件发生时候被释放(详细信息请参见第5条)

(1)手动释放Autorelease pool

(2)Runloop结束后自动释放

对于autorelease pool内部的对象

在引用计数的retain==0的时候释放。

release和autorelease pool的drain都会触发retain–事件。

7.6 autorelease使用注意

1)并不是放到自动释放池代码中,都会自动加入到自动释放池

 @autoreleasepool {
// 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
Person *p = [[Person alloc] init];
[p run];
}

2)在自动释放池的外部发送autorelease 不会被加入到自动释放池中

autorelease是一个方法,只有在自动释放池中调用才有效。

@autoreleasepool {
}
// 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会加入释放池
Person *p = [[[Person alloc] init] autorelease]; //没有写在自动释放池内,写在了自动释放池外
[p run]; // 正确写法1
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
} // 正确写法2
Person *p = [[Person alloc] init];
@autoreleasepool {
[p autorelease];
}

自动释放池的嵌套使用

  • 自动释放池是以栈(栈结构)的形式存在的
  • 由于栈只有一个入口, 所以调用autorelease会将对象放到栈顶的自动释放池
    •   >栈顶是离调用autorelease方法最近的自动释放池
@autoreleasepool { // 栈底自动释放池

    @autoreleasepool {

        @autoreleasepool { // 栈顶自动释放池

        Person *p = [[[Person alloc] init] autorelease];
}
Person *p = [[[Person alloc] init] autorelease];
}
}
  • 自动释放池中不适宜放占用内存比较大的对象
    • 尽量要避免将大内存对象加入释放池,因为释放池的延迟释放机制,会使其一直常驻内存
    • 不要把大量循环操作放到同一个 @autoreleasepool 之间,这样会造成内存峰值的上升
    // 内存峰值会上升
@autoreleasepool {
for (int i = ; i < ; ++i) {
Person *p = [[[Person alloc] init] autorelease];
}
} //可以使用下面的方法
// 内存不会暴涨
for (int i = ; i < ; ++i) {
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
} //创建完一个对象就释放了
}

【写在结尾:】

『学习如逆水行舟,不进则退』