第五章:内存管理
内存管理是Objective-C里重要的概念,要写出内存使用效率高又没有bug的代码,就需要掌握其内存管理的种种细节。Objective-C的内存管理因为有自动引用计数,所以并不复杂,ARC几乎把所有的内存管理的事情都交给编译器决定,作为开发者只需要专注于业务逻辑。
第29条:理解引用计数
- Objective-C对象使用引用计数来管理内存,每个对象都有一个可以递增或者递减的计数器,如果想使对象继续存活就要递增其引用计数,用完了之后就需要递减。
- 在ARC中,所有与引用计数有关的方法的无法编译。虽然不能直接调用,但是理解这些方法能够帮助我们去理解引用计数的概念,ARC也是一种引用计数机制。
- 引用计数的原理:NSObject协议生命了3个方法用于操作计数器
retain: 递增保留计数
release: 递减保留计数
autorealease: 待稍后清理"自动释放池"时,再递减保留计数
- 保留计数至少为1,当对象被初始化之后,对象的引用计数为1,然后如果被强引用,那么表示其他对象想令其继续存活,就可以保留(引用计数+1),等到用完之后,再释放(引用计数-1),当引用计数降至0的时候,那么对象所占的内存也许被回收,一般为了保证不会出现可能指向无效对象的指针(悬挂指针),可以在realease之后清空指针。
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;
- 属性存取方法的内存管理:如果属性为
strong
的修饰符修饰,那么设置的属性值会保留:
- (void)setFoo:(id)foo {
[foo retain];//保留新值
[_foo release];//释放旧值
_foo = foo;//让实例变量指向新值
}
- 自动释放池:调用autorelease会在稍后递减引用计数,通常是下一次事件循环(event loop),也有可能更早。
/*
使用以下的方法会让引用计数+1,而没有释放操作
*/
- (NSString *)stringValue {
NSString *str = [NSString stringWithFormat:@"Test String Value"];
return str;
}
/*
使用autorelease能够很好的避免这种情况,给调用者留下足够的时间。
*/
- (NSString *)stringValue {
NSString *str = [NSString stringWithFormat:@"Test String Value"];
return [str autorelease];
}
- 保留环,如果几个对象相互引用,这样会导致内存泄漏,因为循环中的对象其引用计数不会降为0。通常的解决方案是使用弱引用代替强引用。
第30条:以ARC简化引用计数
- ARC就是自动引用计数,引用计数实际上还是要执行的,只不过保留和释放操作都是由ARC自动为你添加,所以不能直接在ARC下调用内存管理相关的方法:
retain
、release
、autorelease
、dealloc
。而且ARC调用的并不是Objective-C的这些方法,而是和这些方法等效的C语言版本,这样可以节省更多的CPU周期。 - 使用ARC必须遵循的方法命名规则:如果使用以下词语开头的方法,其返回对象归调用者所有:
alloc
、new
、copy
、mutableCopy
。归调用者所有的意思是:调用上述四种方法的那段代码要负责释放所返回的对象。 - 若方法名不以上述四个词语开头,则表示其返回的对象并不归调用者所有,这种情况下所返回的对象会自动释放。
- ARC具有向后兼容的特性,他可以兼容那些不使用ARC的代码。ARC有特殊的函数能优化函数:
+ (EOCPerson *)personWithName:(NSString *)name {
EOCPerson *person = [[EOCPerson alloc] init];
person.name = name;
objc_autoreleaseReturnValue(person);
}
//code using EOCPerson Class
EOCPerson *tmp = [EOCPerson personWithName:@"GDGD"];
_myperson = objc_retainAutoreleasedReturnValue(tmp);
id objc_autoreleaseReturnValue(id object) {
if (/* caller will retain object */) {
set_flag(object);//设置标志位,表示对象要执行retain操作
return object; // No autorelease;
}
else {
return [object autorelease];
}
}
id objc_retainautoreleasedReturnValue(id object) {
if (get_flag(object)) {
clear_flag(object);
return object;//No retain;
}
else {
return [object retain];
}
}
将内存管理交给编译器和运行期组件来做,代码可以得到多种优化,上面只是其中一种。
- 变量的内存管理语义:ARC会用一种安全的方式来设置,先保留新值,在释放旧值,最后设置实例变量。而且使用了ARC之后无需考虑这些情况。
__strong:默认语义,保留此值;
__unsafe_unretained:不保留此值,这样可能不安全,等到再次使用变量的时候,对象可能已经被回收了;
__weak:不保留此值,但是变量可以安全使用,如果对象被回收,那变量也会自动清空;
__autoreleasing:把对象按引用传递给方法的时候,使用这个特殊的修饰符,在返回时自动释放;
使用__weak
和__unsafe_unretained
可以打破保留环。
- 在手动引用计数的时候,可以这样编写
dealloc
方法:
- (void)dealloc {
[_foo release];
[_bar release];
[super dealloc];
}
在ARC里不能直接调用dealloc
,但是非Objective-C对象还是需要清理,可以这样写dealloc
方法:
- (void)dealloc {
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob);
}
- 如果不使用ARC,可以覆写内存管理方法。例如将单例模式下的
release
方法替换为空操作。
第31条:在dealloc方法中只释放引用并解除监听
- 对象在声明周期结束后,会被系统回收,就需要执行
dealloc
方法了,这个方法在对象的引用计数降为0的时候被执行。 dealloc
主要做以下几个事情:- 释放所有的Objective-C对象,ARC会通过自动生成的.cxx_destruct方法在
dealloc
中自动添加这些释放方法。 - 手动释放非Objective-C对象。
- 将配置过的观测行为都清理掉,如果把通知发给了释放的对象,那么会令应用程序崩溃。
- 释放所有的Objective-C对象,ARC会通过自动生成的.cxx_destruct方法在
- 在iOS系统里,应用程序终止时则会调用
UIApplicationDelegate
中的下述方法:
- (void)applicationWillTerminate:(UIApplication *)application
- 尽量不要在
dealloc
里调用其他方法。执行异步任务的方法不应该在dealloc
里调用,只能在正常状态下执行的方法也不应在dealloc
里调用,因为此时对象已经处于正在回收的状态了。
第32条:编写“异常安全代码”时留意内存管理问题
- 在Objective-C的错误模型里,异常应该在发生严重错误后抛出。
- 如果使用手动引用计数,就需要手动处理异常的时候的内存管理。
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomething];
//如果把释放放在这里的话,那么可能在doSomething发生异常,导致内存泄漏
}
@catch(...) {
NSLog(@"...");
}
@finally {
[object release];
}
- 在ARC里,由于不能手动管理内存,所以如果异常出现对象并不会被释放,这样可能出现异常泄漏,虽然也可以开启手动内存管理,但是默认不开启的原因是因为:在Objective-C中,只有引用程序因异常状况而终止时才应该抛出异常,如果应用程序终止了,是否发生内存泄漏就已经无关紧要了。所以如果去处理反而会影响运行效率。
第33条:以弱引用避免保留环
- 如果对象之间以某种方式互相引用,从而形成环,因为Objective-C采用引用计数的内存管理机制,这样就会导致内存泄漏。
- 一个最简单的保留环:
#import <Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;
@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end
@interface EOCClassB : NSObject
@property (nonatomic, strong) EOCClassA *other;
@end
避免保留环最佳方式就是弱引用,这种引用表示非拥有关系,不会让引用计数+1。
@property (nonatomic, unsafe_unretained) EOCClassA *other;
//或者
@property (nonatomic, weak) EOCClassA *other;
属性特质的unsafe_unretained
表明属性可能不安全,如果系统已经把属性所指的对象回收了,那么在其上调用方法可能会导致应用程序崩溃,因为本对象不保留属性对象,因此有可能为系统所回收。
使用weak
的话,就可以避免这种情况,它的作用和unsafe_unretained
相同,但是在属性被系统回收后,属性值会自动设置为nil。
也就是说:当只想EOCClassA
实例的引用移除后,unsafe_unretained
属性仍然指向被回收的那个实例,但是weak
指向nil。
第34条:以“自动释放池块”降低内存峰值
-
释放对象有两种方式:
- 调用release方法,使其引用计数立即递减。
- 调用autorelease方法,将其加入“自动释放池”中。清空自动释放池的时候,系统会向其中的对象发送release消息。
//自动释放池的方法: @autoreleasepool { //... }
-
一般来说不需要自己创建自动释放池,通常只需要在main函数里使用自动释放池来包裹应用程序的主入口点。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
这个池可以理解为最外围捕捉全部自动释放对象所用的池。
-
自动释放池可以嵌套,它于左花括号处创建,并对应右花括号处自动清空,位于自动释放池内的对象,在范围的末尾处收到release消息。自动释放池嵌套使用的好处是可以控制应用程序的内存峰值,使其不至过高。
如果要从数据库里读取数据,那么可能在读取的时候会有新对象创建并加入自动释放池,这样所有对象要等到for循环执行完才会释放,导致内存过高。
//通过下面的方式,加入EOCPerson在创建的时候同时还有很多临时对象的创建,那么内存中会有很多不必要的临时对象没有被释放,导致内存过高
NSArray *databaseRecords = /* record from database */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
//要解决这个问题,增加一个自动释放池就可以
NSArray *databaseRecords = /* record from database */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
//这样循环中自动释放的对象就会放在这个池子,而不是线程的主池里
- 自动释放池的也有开销,尽量不要建立额外的自动释放池。
@autoreleasepool
的方式能够更方便的创建出自动释放池。
第35条:用”僵尸对象“调试内存管理问题
-
主要是使用instrument来进行调试,这里不做详述,贴一个我觉得写的不错的博客吧。
第36条:不要使用retainCount
- NSObject协议里定义了一个方法,可以查询当前的引用计数,这个方法在ARC中是不可用的。
- (NSUInteger)retainCount
- 这个方法是无用的,因为所返回的引用计数只是某个时间点上的值,并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去。
- 同时这个方法可能永远不返回0,因为可能在保留计数还是1的时候,就被系统回收了。