解铃还须系铃人
--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)对象的所有权及引用计数
对象所有权概念:
任何对象都可能拥有一个或多个所有者。只要一个对象至少还拥有一个所有者,它就会继续存在。
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右侧会出现如图设置信息
b. 在同时选中Build Settings和Levels时在右侧搜索框搜索auto,此时会出现ARC设置
c.把ARC设置中目标target下的Yes设置为No,其他相应的Yes也变为No,此时就是MRC管理
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;
给空指针发送消息不会有反应
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
输出-->
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];
} //创建完一个对象就释放了
}
【写在结尾:】
『学习如逆水行舟,不进则退』