手动内存管理
内存泄露:我们在用面向对象的语言编程时,会不断地创造对象,随着时间的推移,有些老的对象不再会被调用,也不再有指针指向它们,但如果程序没有回收它们占用的内存,就会出现形参内存泄露。
有效的内存管理包括2个方面
- 内存分配(相对容易)——合理设计,尽量减少对象的创建,减少创建过程中对内存的开销
- 内存回收——程序不再需要该对象时,必须及时回收它们占用的内存。
内存回收策略有2种:
- 自动回收
- 混合回收——自动+手动
objective-c 可用的内存回收机制有3种:
1. 手动引用计数和自动释放池
2. 自动引用计数(ARC)——全称:automatic reference counting
3. 自动垃圾回收——ios 系统不支持
对象的引用计数
OC用引用计数( 对象关联的整数,该整数称为引用计数器)来跟踪对象的状态,正常情况下,当一段代码访问某个对象时,该对象的引用计数+1,当这段代码不再访问该对象时,该对象的引用计数-1,表示这段代码不再访问该对象。当引用计数的数值为0时,表示程序已经不再需要该对象,系统会自动回收该对象占用的内存。
系统在销毁该对象 之前,会调用 dealloc方法来执行回收操作,所有的对象都有该方法——继承自 NSObject。
注意:当对象被销毁后(引用计数为0,系统调用 dealloc 方法),此时该对象已经不再存在,若有指针指向这个销毁的对象,这个指针就称为悬空指针(dangling pointer)。
程序员不要主动调用该对象的 dealloc 方法!!!
手动引用计数中改变对象的引用计数的方式和一些方法:
1. 当程序调用以 alloc,new,copy,mutableCopy开头的方法(以驼峰写法开头的方法)创建对象时,该对象的引用计数+1;
2. 程序调用对象的 retain 方法时,该对象的引用计数+1——retain 方法返回调用该方法的对象本身,所以在调用该方法后,可直接在此调用其他方法
3. 程序调用对象的 release 方法时,该对象的引用计数-1;
4. autorelease方法: 不改变该对象的 引用计数器的值,只是将对象添加到自动释放池中。
5. retainCount 方法:返回该对象的引用计数的值。
示例程序:
FKItem.h
#import <Foundation/Foundation.h>
// 定义类的接口部分
@interface FKItem : NSObject
@end
FKItem.m
#import "FKItem.h"
// 为FKItem提供实现部分
@implementation FKItem
// 重写该方法作为测试
- (id) init
{
if(self == [super init])
{
NSLog(@"init方法中,引用计数为:%ld" , self.retainCount);
}
return self;
}
// 重写该方法作为测试
- (void) dealloc
{
NSLog(@"系统开始销毁我了,再见!");
[super dealloc];
}
@end
#import "FKItem.h"
// 为FKItem提供实现部分
@implementation FKItem
// 重写该方法作为测试
- (id) init
{
if(self == [super init])
{
NSLog(@"init方法中,引用计数为:%ld" , self.retainCount);
}
return self;
}
// 重写该方法作为测试
- (void) dealloc
{
NSLog(@"系统开始销毁我了,再见!");
[super dealloc];
}
@end
FKItemTest.m
#import <Foundation/Foundation.h>
#import "FKItem.h"
int main(int argc , char * argv[])
{
// 调用new方法创建对象,该对象的引用计数为0
FKItem* item = [FKItem new];
NSLog(@"%ld" , item.retainCount);
[item retain]; // 引用计数加1,retainCount为2
NSLog(@"%ld" , item.retainCount);
[item retain]; // 引用计数加1,retainCount为3
NSLog(@"%ld" , item.retainCount);
[item release]; // 引用计数减1,retainCount为2
NSLog(@"%ld" , item.retainCount);
[item retain]; // 引用计数加1,retainCount为3
NSLog(@"%ld" , item.retainCount);
[item release]; // 引用计数减1,retainCount为2
NSLog(@"%ld" , item.retainCount);
[item release]; // 引用计数减1,retainCount为1
NSLog(@"%ld" , item.retainCount);
[item release]; // 引用计数减1,retainCount为0
// 系统会自动调用该对象的dealloc方法来销毁它,
// 后面代码不要再调用item指针的方法,调用item方法看到的都是假象,甚至可能导致程序崩溃
}
编译运行结果为:
2015-09-30 07:46:42.993 923[1173:23728] init方法中,引用计数为:1
2015-09-30 07:46:42.995 923[1173:23728] 1
2015-09-30 07:46:42.995 923[1173:23728] 2
2015-09-30 07:46:42.995 923[1173:23728] 3
2015-09-30 07:46:42.996 923[1173:23728] 2
2015-09-30 07:46:42.996 923[1173:23728] 3
2015-09-30 07:46:42.997 923[1173:23728] 2
2015-09-30 07:46:42.997 923[1173:23728] 1
2015-09-30 07:46:42.997 923[1173:23728] 系统开始销毁我了,再见!
注意:上面的程序是手动的内存回收。需要关闭 ARC。如何关闭 ARC 呢?很简单,请看下图。
手动内存释放的基本思路
基本应该遵循:谁(包括对象,函数等)把对象的引用计数加了1,谁就要负责在”临死”前把该对象的引用计数减一。——任何实体(包括对象,函数等)在结束前应该把其他对象的引用计数恢复到开始前的状态。自动释放池
为了保证函数,方法返回的对象,不会在被返回之前就被销毁,我们需要保证被函数,方法返回的对象能被延迟销毁。为实现延时销毁,方法有2种: 1. 程序每次获取并使用完其他方法,函数返回的对象后,立即调用该对象的 release 方法将函数,方法返回对象的引用计数-1.——这种方法,易理解,但编程复杂 2. 使用自动释放池进行延迟销毁。>自动释放池:是指它是一个存放对象的容器(比如集合),而自动释放池会保证延迟释放该池中所有的对象。
哪些对象应该添加到自动释放池中呢?怎样把对象加到自动释放池中呢?
所有对象都应该添加到自动释放池中——出于自动释放的考虑,这样可以让自动释放池在销毁之前,先销毁池中的所有对象。 要把一个对象添加到自动释放池中,可调用该对象的 autorelease 方法——该方法返回调用该方法的对象本身。 autorelease方法,会把对象的引用计数—1,时间是自动释放池释放时,自动释放池会让池中所有的对象执行 release 方法。 示例代码: 实现延时销毁第一种情况示例:#import <Foundation/Foundation.h>
#import "FKItem.h"
FKItem* productItem()
{
FKItem* item = [FKItem new]; // 引用计数为1
NSLog(@"函数返回之前的引用计数:%ld" , item.retainCount);
// 返回的对象的引用计数为1
return item;
}
int main(int argc , char * argv[])
{
// it的引用计数为1
FKItem* it = productItem();
// 接下来可以调用it的方法
NSLog(@"%ld" , it.retainCount);
// ...
// 程序执行完成后,将it(引用productItem()函数返回值)的引用计数减1
// 程序使用productItem()函数返回值完成后,延迟销毁it所指向的对象
[it release];
}
实现延时销毁第二种情况:
#import <Foundation/Foundation.h>
#import "FKItem.h"
#import "FKUser.h"
FKItem* productItem()
{
FKItem* item = [FKItem new]; // 引用计数为1
NSLog(@"函数返回之前的引用计数:%ld" , item.retainCount);
// autorelease不会改变对象的引用计数
// 但程序执行autorelease方法时,会将该对象添加到自动释放池中
return [item autorelease];
}
int main(int argc , char * argv[])
{
//创建自动释放池
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// it的引用计数为1
FKItem* it = productItem();
// 接下来可以调用it的方法
NSLog(@"%ld" , it.retainCount);
// ...
// 创建一个FKUser对象,并将它添加到自动释放池
FKUser* user = [[[FKUser alloc] init] autorelease];
// 接下来可以调用user的方法
NSLog(@"%ld" , user.retainCount);
// ...
// 系统将因为池的引用计数变为0而回收自动释放池,
// 回收自动释放池时会调用池中所有对象的release方法
[pool release];
}
☆☆☆临时对象与事件循环中的自动释放池
**对于 foundation 中的类而言,当调用方法创建对象时,只要这些方法不是以 alloc,new,copy,mutableCopy开头的,系统就会创建自动释放对象。** 例如如下代码: ` NSMutableArray* arr=[NSMutableArray arrayWithCapacity:20];`什么是临时对象?
arr 就是个自动释放对象,自动释放池释放时,该对象就会被自动释放,这种对象就被称为——临时对象。什么是事件循环?
Cocoa 与 ios 的事件循环,是指当程序中某组事件被激发(如按钮被单击)后,系统将会回调某个方法来 处理该事件。 事件循环的步骤如下: 1. 创建自动释放池 2. 调用事件处理方法 3. 销毁自动释放池 4. 处理完成,方法返回 由上面的步骤可看出,事件循环中自动创建的是自动释放上下文,,所以,事件处理方法中创建的临时对象都会被销毁。如果程序希望在事件处理方法中 对实例变量赋值,那么就需要格外小心———如果赋值的对象是临时对象,那么对象赋值完成和事件处理结束时,该对象已被回收。 如果需要在事件处理结束后依然可以保留临时对象,可采用如下2种方式之一: 1. 将临时对象赋值之前,先调用临时对象的 retain 方法将它的引用计数+1。这里说明一下,retain 指示符只有在 ARC 机制未启用的 情况下有效。 示例代码:#import <UIKit/UIKit.h>
@interface MyView : UIView
@property(nonatomic, retain ) NSMutableArray* data;
@end
- 把临时对象赋值给 retain,strong 或 copy 指示符修饰的属性。——注意:assign,weak,unsafe_unretain指示符除外。
data属性用 copy 指示符也是可以的。copy 指示符对应的 setter 方法:
if (property != newValue)
{
[property release];
property = [newValue copy];
}
上面的代码返回了newValue的副本,会让副本的引用计数+1。上面的做法虽然没有把newValue的引用计数+1,当被赋值的newValue对象为临时对象,自动释放池释放时,newValue会被释放。但留下了副本。
由于MyView类使用 data 保留了 NSMutableArray 对象(该对象引用计数+1),因此,程序需要在 MyView 的 dealloc 方法中将该 NSMutableArray 对象的引用计数-1——重写MyView类的如下方法:
-(void)dealloc
{
[data release];
[super dealloc];
}
手动内存管理的规则总结
- 调用对象的 release 方法并不是销毁对象——只是将该对象的引用计数-1;
- 自动释放池被回收时,自动释放池会依次调用池中每个对象的 release 方法。——若调用该方法后,引用计数为0,对象被销毁,否则,对象可以从自动释放池中”存活”;
- 当程序以 alloc,new,copy,mutableCopy开头的方法创建对象后,该对象的引用计数为1,当不在使用该对象时,调用该 对象的 release或 autorelease 方法。
- 使用 retain 方法为对象增加引用计数,用完后对象需要调用 release 方法来减少该对象的引用计数,并保证 retain 次数与 release 次数相等。
- 如果创建的对象是个临时对象(其他方法创建的),如果在自动释放上下文中国使用该对象,那么使用完成后无需理会该对象的回收——系统自动回收。如果要保留这个临时对象,需手动调用 retain 来增加该临时 对象的引用计数,或将该临时对象赋值给 retain,strong 或 copy指示符修饰的属性。
- 在 Cocoa 或 ios 的事件循环中,每个事件处理方法执行之前会创建自动释放池,如果希望自动释放池被回收后,池中某些对象能”存活”下来,程序必须增加该对象的引用计数,保证该对象的引用计数大于它调用 autorelease 的次数。
自动引用计数
一旦启用 ARC 机制,程序员不被允许在程序中调用对象的 retain,release 方法来改变对象的引用计数。
前面已经介绍了如何启用ARC机制。
在某些情况下,如果项目需要单独对某个文件启用手动引用计数,——单独关闭某个文件的 ARC 机制。那么如何做呢?
如下图:
找到相应的文件后,双击 添加指令:-fno-objc-arc。
@autoreleasepool 块
@autoreleasepool 块定义自动释放池上下文,任何在块中创建的对象,都由 ARC 来自动释放 ,并在自动释放块结束时销毁这些对象。
如果在程序执行过程中,创建了大量的临时对象(如在循环中创建大量临时对象——非常 差的做法),程序可能需要多次释放这些临时对象,此时程序可以考虑将@autoreleasepool 块放在循环体内,如下代码:
for (int i=0; i<100; i++)
{
@autoreleasepool {
//创建临时对象
//调用临时对象方法
//...
}
}
上面代码在循环体每次执行完成时,都会销毁这些临时对象,避免整个循环结束时才去销毁这些临时对象——不便于更好地管理内存。